Compare commits

..

No commits in common. "main" and "main_go" have entirely different histories.

355 changed files with 51978 additions and 30241 deletions

25
.env
View File

@ -2,7 +2,8 @@ APP_NAME=so-manager
APP_ENV=local APP_ENV=local
APP_KEY=base64:ejLwJbt2bEXY9emPUmsurG+X1hzkjTxQQvq2/FO14RY= APP_KEY=base64:ejLwJbt2bEXY9emPUmsurG+X1hzkjTxQQvq2/FO14RY=
APP_DEBUG=true APP_DEBUG=true
APP_URL=https://krgm.so-manager-dev.com/ APP_URL=http://somanager-local.localhost:81/
APP_LOCALE=ja APP_LOCALE=ja
APP_FALLBACK_LOCALE=ja APP_FALLBACK_LOCALE=ja
APP_FAKER_LOCALE=ja_JP APP_FAKER_LOCALE=ja_JP
@ -22,9 +23,9 @@ LOG_LEVEL=debug
DB_CONNECTION=mysql DB_CONNECTION=mysql
DB_HOST=localhost DB_HOST=localhost
DB_PORT=3306 DB_PORT=3306
DB_DATABASE=krgm DB_DATABASE=somanager_admin
DB_USERNAME=krgm_user DB_USERNAME=root
DB_PASSWORD=StrongDbP@ss2 DB_PASSWORD=
SESSION_DRIVER=database SESSION_DRIVER=database
SESSION_LIFETIME=120 SESSION_LIFETIME=120
@ -46,16 +47,14 @@ REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null REDIS_PASSWORD=null
REDIS_PORT=6379 REDIS_PORT=6379
MAIL_MAILER=smtp MAIL_MAILER=log
#MAIL_SCHEME=null MAIL_SCHEME=null
MAIL_HOST=tomatofox9.sakura.ne.jp MAIL_HOST=127.0.0.1
MAIL_PORT=587 MAIL_PORT=2525
MAIL_USERNAME=demo@so-rin.jp MAIL_USERNAME=null
MAIL_PASSWORD=rokuchou4665 MAIL_PASSWORD=null
MAIL_ENCRYPTION=tls MAIL_FROM_ADDRESS="hello@so-manager-dev.com"
MAIL_FROM_ADDRESS=demo@so-rin.jp
MAIL_FROM_NAME="${APP_NAME}" MAIL_FROM_NAME="${APP_NAME}"
MAIL_ADMIN=demo@so-rin.jp
AWS_ACCESS_KEY_ID= AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY= AWS_SECRET_ACCESS_KEY=

64
.env.main Normal file
View File

@ -0,0 +1,64 @@
APP_NAME=so-manager
APP_ENV=local
APP_KEY=base64:ejLwJbt2bEXY9emPUmsurG+X1hzkjTxQQvq2/FO14RY=
APP_DEBUG=true
APP_URL=https://krgm.so-manager-dev.com/
APP_LOCALE=ja
APP_FALLBACK_LOCALE=ja
APP_FAKER_LOCALE=ja_JP
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=krgm
DB_USERNAME=krgm_user
DB_PASSWORD=StrongDbP@ss2
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@so-manager-dev.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

View File

@ -1,81 +0,0 @@
<?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("");
}
}

View File

@ -0,0 +1,238 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Models\Batch\BatchLog;
use App\Models\Device;
/**
* SHJ-8 バッチ処理ログ登録コマンド
*
* 統一BatchLogを使用してバッチ処理の実行ログをbatch_logテーブルに登録する
* 仕様書に基づくSHJ-8の要求パラメータを受け取り、通用のログシステムで記録
*/
class ShjBatchLogCommand extends Command
{
/**
* コンソールコマンドの名前とシグネチャ
*
* 引数:
* - device_id: デバイスID (必須)
* - process_name: プロセス名 (必須)
* - job_name: ジョブ名 (必須)
* - status: ステータス (必須)
* - created_date: 登録日時 (必須、yyyy/mm/dd形式)
* - updated_date: 更新日時 (必須、yyyy/mm/dd形式)
*
* @var string
*/
protected $signature = 'shj:batch-log {device_id : デバイスID} {process_name : プロセス名} {job_name : ジョブ名} {status : ステータス} {created_date : 登録日時} {updated_date : 更新日時}';
/**
* コンソールコマンドの説明
*
* @var string
*/
protected $description = 'SHJ-8 バッチ処理ログ登録 - バッチ処理の実行ログを登録';
/**
* コンストラクタ
*/
public function __construct()
{
parent::__construct();
}
/**
* コンソールコマンドを実行
*
* 処理フロー:
* 1. 入力パラメーターをチェックする
* 2. 統一BatchLogを使用してbatch_logテーブルに記録
* 3. 仕様書準拠の処理結果を返却する
*
* @return int
*/
public function handle()
{
try {
// 開始ログ出力
$startTime = now();
$this->info('SHJ-8 バッチ処理ログ登録を開始します。');
// 引数取得
$deviceId = (int) $this->argument('device_id');
$processName = $this->argument('process_name');
$jobName = $this->argument('job_name');
$status = $this->argument('status');
$createdDate = $this->argument('created_date');
$updatedDate = $this->argument('updated_date');
Log::info('SHJ-8 バッチ処理ログ登録開始', [
'start_time' => $startTime,
'device_id' => $deviceId,
'process_name' => $processName,
'job_name' => $jobName,
'status' => $status,
'created_date' => $createdDate,
'updated_date' => $updatedDate
]);
// 【処理1】入力パラメーターをチェックする
$paramCheckResult = $this->validateParameters($deviceId, $processName, $jobName, $status, $createdDate, $updatedDate);
if (!$paramCheckResult['valid']) {
$this->error('パラメータエラー: ' . $paramCheckResult['message']);
// 仕様書【判断1】パラメーターNG時の結果出力
$this->line('処理結果: 1'); // 1 = 異常終了
$this->line('異常情報: ' . $paramCheckResult['message']);
return self::FAILURE;
}
// 【処理2】統一BatchLogを使用してログ登録
$batchLog = BatchLog::createBatchLog(
$processName, // 実際のプロセス名を使用
$status,
[
'device_id' => $deviceId,
'job_name' => $jobName,
'status_comment' => BatchLog::getSuccessComment(),
'input_created_date' => $createdDate,
'input_updated_date' => $updatedDate,
'shj8_params' => [
'device_id' => $deviceId,
'process_name' => $processName,
'job_name' => $jobName,
'status' => $status,
'created_date' => $createdDate,
'updated_date' => $updatedDate
]
],
$jobName . '' . BatchLog::getSuccessComment()
);
$endTime = now();
$this->info('SHJ-8 バッチ処理ログ登録が正常に完了しました。');
$this->info("処理時間: {$startTime->diffInSeconds($endTime)}");
Log::info('SHJ-8 バッチ処理ログ登録完了', [
'end_time' => $endTime,
'duration_seconds' => $startTime->diffInSeconds($endTime),
'batch_log_id' => $batchLog->id
]);
// 仕様書【処理3】正常終了時の結果出力
$this->line('処理結果: 0'); // 0 = 正常終了
$this->line('異常情報: '); // 正常時は空文字
return self::SUCCESS;
} catch (\Exception $e) {
$this->error('SHJ-8 バッチ処理ログ登録で予期しないエラーが発生しました: ' . $e->getMessage());
Log::error('SHJ-8 バッチ処理ログ登録例外エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
// 仕様書【処理3】異常終了時の結果出力
$this->line('処理結果: 1'); // 1 = 異常終了
$this->line('異常情報: エラー: ' . $e->getMessage());
return self::FAILURE;
}
}
/**
* 【処理1】パラメータの妥当性を検証
*
* 仕様書に基づく検証内容:
* - デバイスID: 必須、数値、device表に存在するか
* - プロセス名: 「プロセス名」「ジョブ名」いずれか必須
* - ジョブ名: 「プロセス名」「ジョブ名」いずれか必須
* - ステータス: 必須
* - 登録日時: 必須、yyyy/mm/dd形式
* - 更新日時: 必須、yyyy/mm/dd形式
*
* @param int $deviceId デバイスID
* @param string $processName プロセス名
* @param string $jobName ジョブ名
* @param string $status ステータス
* @param string $createdDate 登録日時
* @param string $updatedDate 更新日時
* @return array 検証結果 ['valid' => bool, 'message' => string]
*/
private function validateParameters(int $deviceId, string $processName, string $jobName, string $status, string $createdDate, string $updatedDate): array
{
// デバイスID存在チェック
if ($deviceId <= 0) {
return [
'valid' => false,
'message' => 'パラメーターNG: デバイスIDは正の整数である必要があります'
];
}
if (!Device::exists($deviceId)) {
return [
'valid' => false,
'message' => "パラメーターNG: デバイスID {$deviceId} が存在しません"
];
}
// プロセス名とジョブ名のいずれか必須チェック
if (empty($processName) && empty($jobName)) {
return [
'valid' => false,
'message' => 'パラメーターNG: プロセス名またはジョブ名のいずれかは必須です'
];
}
// ステータス必須チェック
if (empty($status)) {
return [
'valid' => false,
'message' => 'パラメーターNG: ステータスは必須です'
];
}
// 日付形式チェック
if (!$this->isValidDateFormat($createdDate)) {
return [
'valid' => false,
'message' => 'パラメーターNG: 登録日時の形式が正しくありませんyyyy/mm/dd'
];
}
if (!$this->isValidDateFormat($updatedDate)) {
return [
'valid' => false,
'message' => 'パラメーターNG: 更新日時の形式が正しくありませんyyyy/mm/dd'
];
}
return [
'valid' => true,
'message' => 'パラメーターチェックOK'
];
}
/**
* 日付形式の検証
*
* @param string $date 日付文字列
* @return bool 有効な日付形式かどうか
*/
private function isValidDateFormat(string $date): bool
{
// yyyy/mm/dd形式の正規表現チェック
if (!preg_match('/^\d{4}\/\d{2}\/\d{2}$/', $date)) {
return false;
}
// 実際の日付として有効かチェック
$dateParts = explode('/', $date);
return checkdate((int)$dateParts[1], (int)$dateParts[2], (int)$dateParts[0]);
}
}

View File

@ -0,0 +1,182 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Services\ShjElevenService;
/**
* SHJ-11 現在契約台数集計コマンド
*
* 集計単位每个の契約台数を算出し、ゾーンマスタとの管理処理を実行する
* バックグラウンドで実行される定期バッチ処理
*/
class ShjElevenCommand extends Command
{
/**
* コンソールコマンドの名前とシグネチャ
*
* 引数: なし
* オプション: なし
*
* @var string
*/
protected $signature = 'shj:11';
/**
* コンソールコマンドの説明
*
* @var string
*/
protected $description = 'SHJ-11 現在契約台数集計 - 集計単位每个契約台数を算出しゾーンマスタ管理を実行';
/**
* SHJ-11サービスクラス
*
* @var ShjElevenService
*/
protected $shjElevenService;
/**
* コンストラクタ
*
* @param ShjElevenService $shjElevenService
*/
public function __construct(ShjElevenService $shjElevenService)
{
parent::__construct();
$this->shjElevenService = $shjElevenService;
}
/**
* コンソールコマンドを実行
*
* 処理フロー:
* 1. 集計単位每个の契約台数を算出する
* 2. 取得件数判定
* 3. ゾーンマスタを取得する
* 4. 取得判定とゾーンマスタ登録
* 5. 契約台数チェック(限界台数超過判定)
* 6. 契約台数を反映する
* 7. バッチ処理ログを作成する
*
* @return int
*/
public function handle()
{
try {
// 開始ログ出力
$startTime = now();
$this->info('SHJ-11 現在契約台数集計を開始します。');
Log::info('SHJ-11 現在契約台数集計開始', [
'start_time' => $startTime
]);
// 【処理1】集計単位每个の契約台数を算出する
$this->info('【処理1】集計単位每个の契約台数を算出しています...');
$contractCounts = $this->shjElevenService->calculateContractCounts();
// 【判断1】取得件数判定
$countResults = count($contractCounts);
$this->info("取得件数: {$countResults}");
if ($countResults === 0) {
// 対象なしの結果を設定する
$this->info('契約台数算出対象なしのため処理を終了します。');
// バッチ処理ログを作成
$this->shjElevenService->createBatchLog(
'success',
[],
'契約台数算出対象なし',
0,
0,
0
);
Log::info('SHJ-11 現在契約台数集計完了(対象なし)', [
'end_time' => now(),
'duration_seconds' => $startTime->diffInSeconds(now())
]);
return self::SUCCESS;
}
// 【処理2・3】ゾーンマスタ処理取得・登録・更新
$this->info('【処理2】ゾーンマスタ処理を実行しています...');
$processResult = $this->shjElevenService->processZoneManagement($contractCounts);
// 処理結果確認
if ($processResult['success']) {
$endTime = now();
$this->info('SHJ-11 現在契約台数集計が正常に完了しました。');
$this->info("処理時間: {$startTime->diffInSeconds($endTime)}");
$this->info("処理対象件数: {$countResults}");
$this->info("ゾーン新規作成件数: {$processResult['created_zones']}");
$this->info("ゾーン更新件数: {$processResult['updated_zones']}");
$this->info("限界台数超過件数: {$processResult['over_capacity_count']}");
// 【処理4】バッチ処理ログを作成する
$this->shjElevenService->createBatchLog(
'success',
$processResult['parameters'],
'現在契約台数集計処理完了',
$countResults,
$processResult['created_zones'] + $processResult['updated_zones'],
0
);
Log::info('SHJ-11 現在契約台数集計完了', [
'end_time' => $endTime,
'duration_seconds' => $startTime->diffInSeconds($endTime),
'processed_count' => $countResults,
'created_zones' => $processResult['created_zones'],
'updated_zones' => $processResult['updated_zones'],
'over_capacity_count' => $processResult['over_capacity_count']
]);
return self::SUCCESS;
} else {
$this->error('SHJ-11 現在契約台数集計でエラーが発生しました: ' . $processResult['message']);
// エラー時のバッチログ作成
$this->shjElevenService->createBatchLog(
'error',
$processResult['parameters'] ?? [],
$processResult['message'],
$countResults,
$processResult['created_zones'] ?? 0,
1
);
Log::error('SHJ-11 現在契約台数集計エラー', [
'error' => $processResult['message'],
'details' => $processResult['details'] ?? null
]);
return self::FAILURE;
}
} catch (\Exception $e) {
$this->error('SHJ-11 現在契約台数集計で予期しないエラーが発生しました: ' . $e->getMessage());
// 例外時のバッチログ作成
$this->shjElevenService->createBatchLog(
'error',
[],
'システムエラー: ' . $e->getMessage(),
0,
0,
1
);
Log::error('SHJ-11 現在契約台数集計例外エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return self::FAILURE;
}
}
}

View File

@ -0,0 +1,157 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Services\ShjFourCService;
/**
* SHJ-4C 室割当処理コマンド
*
* 駐輪場の区画別利用率状況に基づく室割当処理を実行する
* バックグラウンドで実行される定期バッチ処理
*/
class ShjFourCCommand extends Command
{
/**
* コンソールコマンドの名前とシグネチャ
*
* 引数:
* - park_id: 駐輪場ID (必須)
* - ptype_id: 駐輪分類ID (必須)
* - psection_id: 車種区分ID (必須)
*
* @var string
*/
protected $signature = 'shj:4c {park_id : 駐輪場ID} {ptype_id : 駐輪分類ID} {psection_id : 車種区分ID}';
/**
* コンソールコマンドの説明
*
* @var string
*/
protected $description = 'SHJ-4C 室割当処理 - ゾーン情報取得及び割当処理を実行';
/**
* SHJ-4Cサービスクラス
*
* @var ShjFourCService
*/
protected $shjFourCService;
/**
* コンストラクタ
*
* @param ShjFourCService $shjFourCService
*/
public function __construct(ShjFourCService $shjFourCService)
{
parent::__construct();
$this->shjFourCService = $shjFourCService;
}
/**
* コンソールコマンドを実行
*
* 処理フロー:
* 1. パラメータ取得と検証
* 2. ゾーン情報取得処理
* 3. 割当判定処理
* 4. バッチログ作成
* 5. 処理結果返却
*
* @return int
*/
public function handle()
{
try {
// 開始ログ出力
$startTime = now();
$this->info('SHJ-4C 室割当処理を開始します。');
Log::info('SHJ-4C 室割当処理開始', [
'start_time' => $startTime,
'park_id' => $this->argument('park_id'),
'ptype_id' => $this->argument('ptype_id'),
'psection_id' => $this->argument('psection_id')
]);
// 引数取得
$parkId = $this->argument('park_id');
$ptypeId = $this->argument('ptype_id');
$psectionId = $this->argument('psection_id');
// パラメータ検証
if (!$this->validateParameters($parkId, $ptypeId, $psectionId)) {
$this->error('パラメータが不正です。');
return self::FAILURE;
}
// SHJ-4C処理実行
$result = $this->shjFourCService->executeRoomAllocation($parkId, $ptypeId, $psectionId);
// 処理結果確認
if ($result['success']) {
$endTime = now();
$this->info('SHJ-4C 室割当処理が正常に完了しました。');
$this->info("処理時間: {$startTime->diffInSeconds($endTime)}");
Log::info('SHJ-4C 室割当処理完了', [
'end_time' => $endTime,
'duration_seconds' => $startTime->diffInSeconds($endTime),
'result' => $result
]);
return self::SUCCESS;
} else {
$this->error('SHJ-4C 室割当処理でエラーが発生しました: ' . $result['message']);
Log::error('SHJ-4C 室割当処理エラー', [
'error' => $result['message'],
'details' => $result['details'] ?? null
]);
return self::FAILURE;
}
} catch (\Exception $e) {
$this->error('SHJ-4C 室割当処理で予期しないエラーが発生しました: ' . $e->getMessage());
Log::error('SHJ-4C 室割当処理例外エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return self::FAILURE;
}
}
/**
* パラメータの妥当性を検証
*
* @param mixed $parkId 駐輪場ID
* @param mixed $ptypeId 駐輪分類ID
* @param mixed $psectionId 車種区分ID
* @return bool 検証結果
*/
private function validateParameters($parkId, $ptypeId, $psectionId): bool
{
// 必須パラメータチェック
if (empty($parkId) || empty($ptypeId) || empty($psectionId)) {
$this->error('全てのパラメータは必須です。');
return false;
}
// 数値形式チェック
if (!is_numeric($parkId) || !is_numeric($ptypeId) || !is_numeric($psectionId)) {
$this->error('全てのパラメータは数値である必要があります。');
return false;
}
// 正の整数チェック
if ($parkId <= 0 || $ptypeId <= 0 || $psectionId <= 0) {
$this->error('全てのパラメータは正の整数である必要があります。');
return false;
}
return true;
}
}

View File

@ -0,0 +1,177 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Services\ShjMailSendService;
/**
* SHJ メール送信処理コマンド
*
* メールテンプレートを使用したメール送信処理を実行する
* バックグラウンドで実行される定期バッチ処理
*/
class ShjMailSendCommand extends Command
{
/**
* コンソールコマンドの名前とシグネチャ
*
* 引数:
* - mail_address: メールアドレス (必須)
* - backup_mail_address: 予備メールアドレス (必須)
* - mail_template_id: メールテンプレートID (必須)
*
* @var string
*/
protected $signature = 'shj:mail-send {mail_address : メールアドレス} {backup_mail_address : 予備メールアドレス} {mail_template_id : メールテンプレートID}';
/**
* コンソールコマンドの説明
*
* @var string
*/
protected $description = 'SHJ メール送信処理 - テンプレートに基づくメール送信を実行';
/**
* SHJメール送信サービスクラス
*
* @var ShjMailSendService
*/
protected $shjMailSendService;
/**
* コンストラクタ
*
* @param ShjMailSendService $shjMailSendService
*/
public function __construct(ShjMailSendService $shjMailSendService)
{
parent::__construct();
$this->shjMailSendService = $shjMailSendService;
}
/**
* コンソールコマンドを実行
*
* 処理フロー:
* 1. 入力パラメーターをチェックする
* 2. メール送信テンプレート情報を取得する
* 3. メールを送信する
* 4. 処理結果を返却する
*
* @return int
*/
public function handle()
{
try {
// 開始ログ出力
$startTime = now();
$this->info('SHJ メール送信処理を開始します。');
Log::info('SHJ メール送信処理開始', [
'start_time' => $startTime,
'mail_address' => $this->argument('mail_address'),
'backup_mail_address' => $this->argument('backup_mail_address'),
'mail_template_id' => $this->argument('mail_template_id')
]);
// 引数取得
$mailAddress = $this->argument('mail_address');
$backupMailAddress = $this->argument('backup_mail_address');
$mailTemplateId = $this->argument('mail_template_id');
// 【処理1】パラメータ検証
if (!$this->validateParameters($mailAddress, $backupMailAddress, $mailTemplateId)) {
$this->error('パラメータが不正です。');
return self::FAILURE;
}
// SHJメール送信処理実行
$result = $this->shjMailSendService->executeMailSend($mailAddress, $backupMailAddress, $mailTemplateId);
// 処理結果確認
if ($result['success']) {
$endTime = now();
$this->info('SHJ メール送信処理が正常に完了しました。');
$this->info("処理時間: {$startTime->diffInSeconds($endTime)}");
Log::info('SHJ メール送信処理完了', [
'end_time' => $endTime,
'duration_seconds' => $startTime->diffInSeconds($endTime),
'result' => $result
]);
return self::SUCCESS;
} else {
$this->error('SHJ メール送信処理でエラーが発生しました: ' . $result['message']);
Log::error('SHJ メール送信処理エラー', [
'error' => $result['message'],
'details' => $result['details'] ?? null
]);
return self::FAILURE;
}
} catch (\Exception $e) {
$this->error('SHJ メール送信処理で予期しないエラーが発生しました: ' . $e->getMessage());
Log::error('SHJ メール送信処理例外エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return self::FAILURE;
}
}
/**
* 【処理1】パラメータの妥当性を検証
*
* 仕様書に基づく検証内容:
* - メールアドレス: 「メールアドレス」「予備メールアドレス」いずれか必須
* - メールテンプレートID: 必須
*
* @param mixed $mailAddress メールアドレス
* @param mixed $backupMailAddress 予備メールアドレス
* @param mixed $mailTemplateId メールテンプレートID
* @return bool 検証結果
*/
private function validateParameters($mailAddress, $backupMailAddress, $mailTemplateId): bool
{
// メールテンプレートIDチェック
if (empty($mailTemplateId)) {
$this->error('メールテンプレートIDは必須です。');
return false;
}
// 数値形式チェックメールテンプレートID
if (!is_numeric($mailTemplateId)) {
$this->error('メールテンプレートIDは数値である必要があります。');
return false;
}
// 正の整数チェックメールテンプレートID
if ($mailTemplateId <= 0) {
$this->error('メールテンプレートIDは正の整数である必要があります。');
return false;
}
// メールアドレスチェック(いずれか必須)
if (empty($mailAddress) && empty($backupMailAddress)) {
$this->error('メールアドレスまたは予備メールアドレスのいずれかは必須です。');
return false;
}
// メールアドレス形式チェック
if (!empty($mailAddress) && !filter_var($mailAddress, FILTER_VALIDATE_EMAIL)) {
$this->error('メールアドレスの形式が正しくありません。');
return false;
}
if (!empty($backupMailAddress) && !filter_var($backupMailAddress, FILTER_VALIDATE_EMAIL)) {
$this->error('予備メールアドレスの形式が正しくありません。');
return false;
}
return true;
}
}

View File

@ -0,0 +1,206 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Services\ShjNineService;
/**
* SHJ-9 売上集計処理コマンド
*
* 駐輪場の売上データを日次・月次・年次で集計する処理を実行する
* バックグラウンドで実行される定期バッチ処理
*/
class ShjNineCommand extends Command
{
/**
* コンソールコマンドの名前とシグネチャ
*
* 引数:
* - type: 集計種別 (daily/monthly/yearly) (必須)
* - target_date: 集計対象日 (オプション、YYYY-MM-DD形式)
*
* @var string
*/
protected $signature = 'shj:9 {type : 集計種別(daily/monthly/yearly)} {target_date? : 集計対象日(YYYY-MM-DD)}';
/**
* コンソールコマンドの説明
*
* @var string
*/
protected $description = 'SHJ-9 売上集計処理 - 日次/月次/年次売上データ集計を実行';
/**
* SHJ-9サービスクラス
*
* @var ShjNineService
*/
protected $shjNineService;
/**
* コンストラクタ
*
* @param ShjNineService $shjNineService
*/
public function __construct(ShjNineService $shjNineService)
{
parent::__construct();
$this->shjNineService = $shjNineService;
}
/**
* コンソールコマンドを実行
*
* 処理フロー:
* 1. パラメータ取得と検証
* 2. 集計対象日設定
* 3. 売上集計処理実行
* 4. バッチログ作成
* 5. 処理結果返却
*
* @return int
*/
public function handle()
{
try {
// 開始ログ出力
$startTime = now();
$this->info('SHJ-9 売上集計処理を開始します。');
// 引数取得
$type = $this->argument('type');
$targetDate = $this->argument('target_date');
Log::info('SHJ-9 売上集計処理開始', [
'start_time' => $startTime,
'type' => $type,
'target_date' => $targetDate
]);
// パラメータ検証
if (!$this->validateParameters($type, $targetDate)) {
$this->error('パラメータが不正です。');
return self::FAILURE;
}
// 集計対象日設定
$aggregationDate = $this->determineAggregationDate($type, $targetDate);
$this->info("集計種別: {$type}");
$this->info("集計対象日: {$aggregationDate}");
// SHJ-9処理実行
$result = $this->shjNineService->executeEarningsAggregation($type, $aggregationDate);
// 処理結果確認
if ($result['success']) {
$endTime = now();
$this->info('SHJ-9 売上集計処理が正常に完了しました。');
$this->info("処理時間: {$startTime->diffInSeconds($endTime)}");
$this->info("処理結果: 駐輪場数 {$result['processed_parks']}, 集計レコード数 {$result['summary_records']}");
Log::info('SHJ-9 売上集計処理完了', [
'end_time' => $endTime,
'duration_seconds' => $startTime->diffInSeconds($endTime),
'result' => $result
]);
return self::SUCCESS;
} else {
$this->error('SHJ-9 売上集計処理でエラーが発生しました: ' . $result['message']);
Log::error('SHJ-9 売上集計処理エラー', [
'error' => $result['message'],
'details' => $result['details'] ?? null
]);
return self::FAILURE;
}
} catch (\Exception $e) {
$this->error('SHJ-9 売上集計処理で予期しないエラーが発生しました: ' . $e->getMessage());
Log::error('SHJ-9 売上集計処理例外エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return self::FAILURE;
}
}
/**
* パラメータの妥当性を検証
*
* @param string $type 集計種別
* @param string|null $targetDate 対象日
* @return bool 検証結果
*/
private function validateParameters(string $type, ?string $targetDate): bool
{
// 集計種別チェック
$allowedTypes = ['daily', 'monthly', 'yearly'];
if (!in_array($type, $allowedTypes)) {
$this->error('集計種別は daily, monthly, yearly のいずれかを指定してください。');
return false;
}
// 対象日形式チェック(指定されている場合)
if ($targetDate && !$this->isValidDateFormat($targetDate)) {
$this->error('対象日の形式が正しくありませんYYYY-MM-DD形式で指定してください。');
return false;
}
return true;
}
/**
* 集計対象日を決定
*
* @param string $type 集計種別
* @param string|null $targetDate 指定日
* @return string 集計対象日
*/
private function determineAggregationDate(string $type, ?string $targetDate): string
{
if ($targetDate) {
return $targetDate;
}
// パラメータ指定がない場合のデフォルト設定
switch ($type) {
case 'daily':
// 日次昨日本日の1日前
return now()->subDay()->format('Y-m-d');
case 'monthly':
// 月次:前月の最終日
return now()->subMonth()->endOfMonth()->format('Y-m-d');
case 'yearly':
// 年次:前年の最終日
return now()->subYear()->endOfYear()->format('Y-m-d');
default:
return now()->subDay()->format('Y-m-d');
}
}
/**
* 日付形式の検証
*
* @param string $date 日付文字列
* @return bool 有効な日付形式かどうか
*/
private function isValidDateFormat(string $date): bool
{
// YYYY-MM-DD形式の正規表現チェック
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
return false;
}
// 実際の日付として有効かチェック
$dateParts = explode('-', $date);
return checkdate((int)$dateParts[1], (int)$dateParts[2], (int)$dateParts[0]);
}
}

View File

@ -0,0 +1,129 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Services\ShjSixService;
/**
* SHJ-6 サーバ死活監視処理コマンド
*
* サーバとデバイスの死活監視を行い、異常時にはメール通知を実行する
* 定期実行またはオンデマンド実行のバックグラウンドバッチ処理
*/
class ShjSixCommand extends Command
{
/**
* コンソールコマンドの名前とシグネチャ
*
* パラメータなしで実行
*
* @var string
*/
protected $signature = 'shj:6';
/**
* コンソールコマンドの説明
*
* @var string
*/
protected $description = 'SHJ-6 サーバ死活監視処理 - サーバ・デバイス監視とアラート通知を実行';
/**
* SHJ-6サービスクラス
*
* @var ShjSixService
*/
protected $shjSixService;
/**
* コンストラクタ
*
* @param ShjSixService $shjSixService
*/
public function __construct(ShjSixService $shjSixService)
{
parent::__construct();
$this->shjSixService = $shjSixService;
}
/**
* コンソールコマンドを実行
*
* 処理フロー:
* 1. サーバ死活監視DBアクセス
* 2. デバイス管理マスタを取得する
* 3. デバイス毎のハードウェア状態を取得する
* 4. プリンタ制御プログラムログを取得する
* 5. バッチ処理ログを作成する
* 異常検出時は共通A処理メール通知を実行
*
* @return int
*/
public function handle()
{
try {
// 開始ログ出力
$startTime = now();
$this->info('SHJ-6 サーバ死活監視処理を開始します。');
Log::info('SHJ-6 サーバ死活監視処理開始', [
'start_time' => $startTime
]);
// SHJ-6監視処理実行
$result = $this->shjSixService->executeServerMonitoring();
// 処理結果確認
if ($result['success']) {
$endTime = now();
$this->info('SHJ-6 サーバ死活監視処理が正常に完了しました。');
$this->info("処理時間: {$startTime->diffInSeconds($endTime)}");
$this->info("監視結果: {$result['monitoring_summary']}");
// 警告がある場合は表示
if (!empty($result['warnings'])) {
$this->warn('警告が検出されました:');
foreach ($result['warnings'] as $warning) {
$this->warn("- {$warning}");
}
}
Log::info('SHJ-6 サーバ死活監視処理完了', [
'end_time' => $endTime,
'duration_seconds' => $startTime->diffInSeconds($endTime),
'result' => $result
]);
return self::SUCCESS;
} else {
$this->error('SHJ-6 サーバ死活監視処理でエラーが発生しました: ' . $result['message']);
// エラー詳細があれば表示
if (!empty($result['error_details'])) {
$this->error('エラー詳細:');
foreach ($result['error_details'] as $detail) {
$this->error("- {$detail}");
}
}
Log::error('SHJ-6 サーバ死活監視処理エラー', [
'error' => $result['message'],
'details' => $result['error_details'] ?? null
]);
return self::FAILURE;
}
} catch (\Exception $e) {
$this->error('SHJ-6 サーバ死活監視処理で予期しないエラーが発生しました: ' . $e->getMessage());
Log::error('SHJ-6 サーバ死活監視処理例外エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return self::FAILURE;
}
}
}

View File

@ -0,0 +1,236 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Services\ShjTenService;
/**
* SHJ-10 売上集計処理コマンド
*
* 駐輪場の売上データを財政年度ベースで年次・月次集計する処理を実行する
* 4月開始の財政年度期間で計算するバックグラウンドバッチ処理
*/
class ShjTenCommand extends Command
{
/**
* コンソールコマンドの名前とシグネチャ
*
* 引数:
* - type: 集計種別 (yearly/monthly) (必須)
* - target: 集計対象 (必須)
* - yearly: 年度 (: 2019)
* - monthly: 年月 (: 2019/01)
*
* @var string
*/
protected $signature = 'shj:10 {type : 集計種別(yearly/monthly)} {target : 集計対象(yearly:年度, monthly:年月)}';
/**
* コンソールコマンドの説明
*
* @var string
*/
protected $description = 'SHJ-10 売上集計処理 - 財政年度ベース年次/月次売上データ集計を実行';
/**
* SHJ-10サービスクラス
*
* @var ShjTenService
*/
protected $shjTenService;
/**
* コンストラクタ
*
* @param ShjTenService $shjTenService
*/
public function __construct(ShjTenService $shjTenService)
{
parent::__construct();
$this->shjTenService = $shjTenService;
}
/**
* コンソールコマンドを実行
*
* 処理フロー:
* 1. パラメータ取得と検証
* 2. 財政年度期間設定
* 3. 売上集計処理実行
* 4. バッチログ作成
* 5. 処理結果返却
*
* @return int
*/
public function handle()
{
try {
// 開始ログ出力
$startTime = now();
$this->info('SHJ-10 売上集計処理を開始します。');
// 引数取得
$type = $this->argument('type');
$target = $this->argument('target');
Log::info('SHJ-10 売上集計処理開始', [
'start_time' => $startTime,
'type' => $type,
'target' => $target
]);
// パラメータ検証
if (!$this->validateParameters($type, $target)) {
$this->error('パラメータが不正です。');
return self::FAILURE;
}
// 財政年度期間設定
$fiscalPeriod = $this->determineFiscalPeriod($type, $target);
$this->info("集計種別: {$type}");
$this->info("集計対象: {$target}");
$this->info("財政期間: {$fiscalPeriod['start_date']} {$fiscalPeriod['end_date']}");
// SHJ-10処理実行
$result = $this->shjTenService->executeFiscalEarningsAggregation($type, $target, $fiscalPeriod);
// 処理結果確認
if ($result['success']) {
$endTime = now();
$this->info('SHJ-10 売上集計処理が正常に完了しました。');
$this->info("処理時間: {$startTime->diffInSeconds($endTime)}");
$this->info("処理結果: 駐輪場数 {$result['processed_parks']}, 集計レコード数 {$result['summary_records']}");
Log::info('SHJ-10 売上集計処理完了', [
'end_time' => $endTime,
'duration_seconds' => $startTime->diffInSeconds($endTime),
'result' => $result
]);
return self::SUCCESS;
} else {
$this->error('SHJ-10 売上集計処理でエラーが発生しました: ' . $result['message']);
Log::error('SHJ-10 売上集計処理エラー', [
'error' => $result['message'],
'details' => $result['details'] ?? null
]);
return self::FAILURE;
}
} catch (\Exception $e) {
$this->error('SHJ-10 売上集計処理で予期しないエラーが発生しました: ' . $e->getMessage());
Log::error('SHJ-10 売上集計処理例外エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return self::FAILURE;
}
}
/**
* パラメータの妥当性を検証
*
* @param string $type 集計種別
* @param string $target 集計対象
* @return bool 検証結果
*/
private function validateParameters(string $type, string $target): bool
{
// 集計種別チェック
$allowedTypes = ['yearly', 'monthly'];
if (!in_array($type, $allowedTypes)) {
$this->error('集計種別は yearly, monthly のいずれかを指定してください。');
return false;
}
// 集計対象形式チェック
if ($type === 'yearly') {
// 年度形式チェック (例: 2019)
if (!preg_match('/^\d{4}$/', $target)) {
$this->error('年次集計の場合、年度を4桁の数字で指定してください。(例: 2019)');
return false;
}
} elseif ($type === 'monthly') {
// 年月形式チェック (例: 2019/01)
if (!preg_match('/^\d{4}\/\d{2}$/', $target)) {
$this->error('月次集計の場合、年月をYYYY/MM形式で指定してください。(例: 2019/01)');
return false;
}
// 月の範囲チェック (01-12)
$parts = explode('/', $target);
$month = (int)$parts[1];
if ($month < 1 || $month > 12) {
$this->error('月は01から12までの範囲で指定してください。');
return false;
}
}
return true;
}
/**
* 財政年度期間を決定
*
* 財政年度は4月開始Config設定可能
* - yearly 2019: 2019年4月1日 2020年3月31日
* - monthly 2019/01: 2019年1月1日 2019年1月31日
*
* @param string $type 集計種別
* @param string $target 集計対象
* @return array 財政期間情報
*/
private function determineFiscalPeriod(string $type, string $target): array
{
$fiscalStartMonth = 4; // 財政年度開始月4月
if ($type === 'yearly') {
$year = (int)$target;
// 財政年度期間計算
$startDate = sprintf('%04d-%02d-01', $year, $fiscalStartMonth);
$endDate = sprintf('%04d-%02d-%02d', $year + 1, $fiscalStartMonth - 1,
date('t', strtotime(sprintf('%04d-%02d-01', $year + 1, $fiscalStartMonth - 1))));
return [
'type' => 'yearly',
'fiscal_year' => $year,
'start_date' => $startDate,
'end_date' => $endDate,
'summary_type' => 1, // 年次
'target_label' => "{$year}年度"
];
} elseif ($type === 'monthly') {
$parts = explode('/', $target);
$year = (int)$parts[0];
$month = (int)$parts[1];
// 指定月の期間計算
$startDate = sprintf('%04d-%02d-01', $year, $month);
$endDate = sprintf('%04d-%02d-%02d', $year, $month,
date('t', strtotime($startDate)));
// 該当する財政年度を計算
$fiscalYear = $month >= $fiscalStartMonth ? $year : $year - 1;
return [
'type' => 'monthly',
'fiscal_year' => $fiscalYear,
'target_year' => $year,
'target_month' => $month,
'start_date' => $startDate,
'end_date' => $endDate,
'summary_type' => 2, // 月次
'target_label' => "{$year}{$month}"
];
}
throw new \InvalidArgumentException("不正な集計種別: {$type}");
}
}

View File

@ -0,0 +1,177 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Services\ShjTwelveService;
/**
* SHJ-12 未払い者通知処理コマンド
*
* 定期契約マスタより未払い者を取得し、通知処理またはオペレーターキュー追加を実行する
* バックグラウンドで実行される定期バッチ処理
*/
class ShjTwelveCommand extends Command
{
/**
* コンソールコマンドの名前とシグネチャ
*
* 引数: なし
* オプション: なし
*
* @var string
*/
protected $signature = 'shj:12';
/**
* コンソールコマンドの説明
*
* @var string
*/
protected $description = 'SHJ-12 未払い者通知処理 - 定期契約マスタより未払い者を取得し通知処理を実行';
/**
* SHJ-12サービスクラス
*
* @var ShjTwelveService
*/
protected $shjTwelveService;
/**
* コンストラクタ
*
* @param ShjTwelveService $shjTwelveService
*/
public function __construct(ShjTwelveService $shjTwelveService)
{
parent::__construct();
$this->shjTwelveService = $shjTwelveService;
}
/**
* コンソールコマンドを実行
*
* 処理フロー:
* 1. 定期契約マスタより未払い者を取得する
* 2. 取得件数判定
* 3. 未払い者への通知、またはオペレーターキュー追加処理
* 4. バッチ処理ログを作成する
*
* @return int
*/
public function handle()
{
try {
// 開始ログ出力
$startTime = now();
$this->info('SHJ-12 未払い者通知処理を開始します。');
Log::info('SHJ-12 未払い者通知処理開始', [
'start_time' => $startTime
]);
// 【処理1】定期契約マスタより未払い者を取得する
$this->info('【処理1】定期契約マスタより未払い者を取得しています...');
$unpaidUsers = $this->shjTwelveService->getUnpaidUsers();
// 【判断1】取得件数判定
$unpaidCount = count($unpaidUsers);
$this->info("取得件数: {$unpaidCount}");
if ($unpaidCount === 0) {
// 未払い者対象なしの結果を設定する
$this->info('未払い者対象なしのため処理を終了します。');
// バッチ処理ログを作成
$this->shjTwelveService->createBatchLog(
'success',
[],
'未払い者対象なし',
0,
0,
0
);
Log::info('SHJ-12 未払い者通知処理完了(対象なし)', [
'end_time' => now(),
'duration_seconds' => $startTime->diffInSeconds(now())
]);
return self::SUCCESS;
}
// 【処理2】未払い者への通知、またはオペレーターキュー追加処理
$this->info('【処理2】未払い者への通知処理を実行しています...');
$processResult = $this->shjTwelveService->processUnpaidUserNotifications($unpaidUsers);
// 処理結果確認
if ($processResult['success']) {
$endTime = now();
$this->info('SHJ-12 未払い者通知処理が正常に完了しました。');
$this->info("処理時間: {$startTime->diffInSeconds($endTime)}");
$this->info("処理対象件数: {$unpaidCount}");
$this->info("通知送信件数: {$processResult['notification_count']}");
$this->info("オペレーターキュー追加件数: {$processResult['queue_count']}");
// 【処理3】バッチ処理ログを作成する
$this->shjTwelveService->createBatchLog(
'success',
$processResult['parameters'],
'未払い者通知処理完了',
$unpaidCount,
$processResult['notification_count'] + $processResult['queue_count'],
0
);
Log::info('SHJ-12 未払い者通知処理完了', [
'end_time' => $endTime,
'duration_seconds' => $startTime->diffInSeconds($endTime),
'processed_count' => $unpaidCount,
'notification_count' => $processResult['notification_count'],
'queue_count' => $processResult['queue_count']
]);
return self::SUCCESS;
} else {
$this->error('SHJ-12 未払い者通知処理でエラーが発生しました: ' . $processResult['message']);
// エラー時のバッチログ作成
$this->shjTwelveService->createBatchLog(
'error',
$processResult['parameters'] ?? [],
$processResult['message'],
$unpaidCount,
$processResult['notification_count'] ?? 0,
1
);
Log::error('SHJ-12 未払い者通知処理エラー', [
'error' => $processResult['message'],
'details' => $processResult['details'] ?? null
]);
return self::FAILURE;
}
} catch (\Exception $e) {
$this->error('SHJ-12 未払い者通知処理で予期しないエラーが発生しました: ' . $e->getMessage());
// 例外時のバッチログ作成
$this->shjTwelveService->createBatchLog(
'error',
[],
'システムエラー: ' . $e->getMessage(),
0,
0,
1
);
Log::error('SHJ-12 未払い者通知処理例外エラー', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return self::FAILURE;
}
}
}

View File

@ -1,34 +0,0 @@
<?php
if (! function_exists('keepUserListQuery')) {
/**
* 利用者一覧:検索条件保持用
*
* @param array $override 追加・上書きしたいパラメータ
* @return array
*/
function keepUserListQuery(array $override = []): array
{
return array_merge(
request()->only([
'user_id',
'user_categoryid',
'user_tag_serial',
'quit_flag',
'user_category1',
'user_category2',
'user_category3',
'user_phonetic',
'phone',
'email',
'tag_qr_flag',
'quit_from',
'quit_to',
'sort',
'dir',
'page',
]),
$override
);
}
}

View File

@ -1,24 +1,23 @@
<?php <?php
declare(strict_types=1);
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\CityRequest;
use App\Models\City;
use App\Services\CityService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\View\View; use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\DB;
use App\Models\City;
final class CityController extends Controller class CityController extends Controller
{ {
public function index(Request $request, CityService $service): View|RedirectResponse public function list(Request $request)
{ {
$sort = (string) $request->input('sort', 'city_id'); $inputs = [
$sortType = (string) $request->input('sort_type', 'asc'); 'isMethodPost' => $request->isMethod('post'),
$page = (int) $request->get('page', 1); 'sort' => $request->input('sort', ''),
'sort_type' => $request->input('sort_type', ''),
'page' => $request->get('page', 1),
];
$query = City::query(); $query = City::query();
@ -26,30 +25,20 @@ final class CityController extends Controller
$query->where('city_name', 'like', '%' . $request->input('city_name') . '%'); $query->where('city_name', 'like', '%' . $request->input('city_name') . '%');
} }
// 排序处理 if (!empty($inputs['sort'])) {
if (!empty($sort)) { $query->orderBy($inputs['sort'], $inputs['sort_type'] ?? 'asc');
$query->orderBy($sort, $sortType);
} }
$list = $query->paginate(20); $inputs['list'] = $query->paginate(20);
// 页码越界处理 if ($inputs['list']->total() > 0 && $inputs['page'] > $inputs['list']->lastPage()) {
if ($list->total() > 0 && $page > $list->lastPage()) { return redirect()->route('city');
return redirect()->route('cities.index', [
'sort' => $sort,
'sort_type' => $sortType,
]);
} }
return view('admin.cities.index', [ return view('admin.CityMaster.list', $inputs);
'sort' => $sort,
'sort_type' => $sortType,
'list' => $list,
'page' => $page,
]);
} }
public function create(): View public function add(Request $request)
{ {
$inputs = [ $inputs = [
'city_name' => '', 'city_name' => '',
@ -60,21 +49,9 @@ final class CityController extends Controller
if ($request->isMethod('POST')) { if ($request->isMethod('POST')) {
$rules = [ $rules = [
'city_name' => ['required', 'string', 'max:10', 'regex:/^[^ -~。-゚]+$/u'], 'city_name' => 'required|string|max:255',
'print_layout' => ['required', 'string', 'max:10', 'regex:/^[^ -~。-゚]+$/u'],
'city_user' => ['required', 'string', 'max:10', 'regex:/^[^ -~。-゚]+$/u'],
'city_remarks' => ['nullable', 'string', 'max:20'],
]; ];
$messages = [ $validator = Validator::make($request->all(), $rules);
'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()); $inputs = array_merge($inputs, $request->all());
@ -114,21 +91,9 @@ final class CityController extends Controller
if ($request->isMethod('POST')) { if ($request->isMethod('POST')) {
$rules = [ $rules = [
'city_name' => ['required', 'string', 'max:10', 'regex:/^[^ -~。-゚]+$/u'], 'city_name' => 'required|string|max:255',
'print_layout' => ['required', 'string', 'max:10', 'regex:/^[^ -~。-゚]+$/u'],
'city_user' => ['required', 'string', 'max:10', 'regex:/^[^ -~。-゚]+$/u'],
'city_remarks' => ['nullable', 'string', 'max:20'],
]; ];
$messages = [ $validator = Validator::make($request->all(), $rules);
'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()) { if (!$validator->fails()) {
$city->fill($request->only([ $city->fill($request->only([
@ -165,10 +130,7 @@ final class CityController extends Controller
public function delete(Request $request) public function delete(Request $request)
{ {
$arr_pk = $request->get('pk'); $arr_pk = $request->get('pk');
if (!$arr_pk) { if ($arr_pk && City::destroy($arr_pk)) {
return redirect()->route('city')->with('error', __('削除する市区を選択してください。'));
}
if (City::destroy($arr_pk)) {
return redirect()->route('city')->with('success', __("削除が完了しました。")); return redirect()->route('city')->with('success', __("削除が完了しました。"));
} else { } else {
return redirect()->route('city')->with('error', __('削除に失敗しました。')); return redirect()->route('city')->with('error', __('削除に失敗しました。'));

View File

@ -17,22 +17,12 @@ class ContractAllowableCityController extends Controller
* 一覧表示 * 一覧表示
*/ */
public function list(Request $request) public function list(Request $request)
{ {
$inputs = $request->all(); $inputs = $request->all();
$inputs['isMethodPost'] = $request->isMethod('post'); $inputs['isMethodPost'] = $request->isMethod('post');
// 解除処理 // 解除処理
if ($request->isMethod('post') && $request->input('action') === 'unlink') { if ($request->isMethod('post') && $request->input('action') === 'unlink') {
// バリデーション解除条件が1つも入力されていない場合はエラー
if (
!$request->filled('contract_allowable_city_id')
&& !$request->filled('city_id')
&& !$request->filled('contract_allowable_city_name')
&& !$request->filled('park_id')
) {
return back()->withErrors(['解除条件を1つ以上入力してください。']);
}
$query = ContractAllowableCity::query(); $query = ContractAllowableCity::query();
if ($request->filled('contract_allowable_city_id')) { if ($request->filled('contract_allowable_city_id')) {
@ -48,7 +38,11 @@ class ContractAllowableCityController extends Controller
$query->where('park_id', $request->park_id); $query->where('park_id', $request->park_id);
} }
$count = $query->delete(); $records = $query->get();
foreach ($records as $record) {
$record->delete();
}
return redirect()->route('contract_allowable_cities')->with('success', '解除しました'); return redirect()->route('contract_allowable_cities')->with('success', '解除しました');
} }
@ -63,7 +57,8 @@ class ContractAllowableCityController extends Controller
'cityList' => City::getList(), 'cityList' => City::getList(),
'parkList' => Park::getList(), 'parkList' => Park::getList(),
]); ]);
} }
/** /**
* 新規登録 * 新規登録
@ -71,29 +66,30 @@ class ContractAllowableCityController extends Controller
public function add(Request $request) public function add(Request $request)
{ {
if ($request->isMethod('post')) { if ($request->isMethod('post')) {
$validated = $request->validate([ $request->validate([
'city_id' => 'required|integer', 'city_id' => 'required|integer',
'contract_allowable_city_name' => 'required|string|max:20', 'contract_allowable_city_name' => 'required|string|max:20',
'park_id' => 'required|integer', 'park_id' => 'required|integer',
'same_district_flag' => 'required|integer', 'same_district_flag' => 'required|integer',
]); ]);
$validated['operator_id'] = Auth::user()->ope_id; $data = $request->all();
$data['operator_id'] = Auth::user()->ope_id;
ContractAllowableCity::create($validated); ContractAllowableCity::create($data);
return redirect()->route('contract_allowable_cities') return redirect()->route('contract_allowable_cities')->with('success', '登録しました');
->with('success', '登録しました。');
} }
return view('admin.contract_allowable_cities.add', [ return view('admin.contract_allowable_cities.add', [
'record' => null, 'record' => null,
'cityList' => City::getList(), 'cityList' => City::getList(),
'parkList' => Park::getList(), 'parkList' => Park::getList(),
'contractAllowableCityList' => ContractAllowableCity::getList(),
'mode' => 'add'
]); ]);
} }
/** /**
* 編集 * 編集
*/ */
@ -101,104 +97,95 @@ class ContractAllowableCityController extends Controller
{ {
$record = ContractAllowableCity::getByPk($id); $record = ContractAllowableCity::getByPk($id);
if (!$record) { if (!$record) {
return redirect()->route('contract_allowable_cities') return redirect()->route('contract_allowable_cities')->with('error', 'データが存在しません');
->with('error', 'データが存在しません');
} }
if ($request->isMethod('post')) { if ($request->isMethod('post')) {
$validated = $request->validate([ $request->validate([
'city_id' => 'required|integer', 'city_id' => 'required|integer',
'contract_allowable_city_name' => 'required|string|max:20', 'contract_allowable_city_name' => 'required|string|max:20',
'park_id' => 'required|integer', 'park_id' => 'required|integer',
'same_district_flag' => 'required|integer', 'same_district_flag' => 'required|integer',
]); ]);
$record->fill($validated); $record->fill($request->all());
$record->operator_id = Auth::user()->ope_id; $record->operator_id = Auth::user()->ope_id;
$record->save(); $record->save();
return redirect()->route('contract_allowable_cities') return redirect()->route('contract_allowable_cities')->with('success', '更新しました');
->with('success', '更新しました。'); }
return view('admin.contract_allowable_cities.edit', [
'record' => $record,
'cities' => City::getList(),
'parks' => Park::getList(),
'mode' => 'edit'
]);
}
/**
* 詳細参照(表示のみ)
*/
public function info($id)
{
$record = ContractAllowableCity::getByPk($id);
if (!$record) {
return redirect()->route('contract_allowable_cities')->with('error', 'データが存在しません');
} }
return view('admin.contract_allowable_cities.edit', [ return view('admin.contract_allowable_cities.edit', [
'record' => $record, 'record' => $record,
'cityList' => City::getList(), 'cityList' => City::getList(),
'parkList' => Park::getList(), 'parkList' => Park::getList(),
'mode' => 'info'
]); ]);
} }
/** /**
* 一括削除(単一・複数対応) * 一括削除
*/ */
public function delete(Request $request) public function delete(Request $request)
{ {
// バリデーション:'id'は必須、配列の場合は各要素が整数 if ($request->has('id')) {
$request->validate([ ContractAllowableCity::deleteByPk($request->id);
'id' => 'required', return redirect()->route('contract_allowable_cities')->with('success', '削除しました');
'id.*' => 'integer',
]);
// idを配列化単一でも複数でも対応
$ids = (array)$request->input('id');
// 削除処理
// ContractAllowableCity::destroy($ids) が使える場合
$deleted = ContractAllowableCity::destroy($ids);
// 削除件数でメッセージ分岐
if ($deleted > 0) {
return redirect()->route('contract_allowable_cities')->with('success', '削除しました。');
} else {
return redirect()->route('contract_allowable_cities')->with('error', '削除に失敗しました。');
} }
return redirect()->route('contract_allowable_cities')->with('error', '削除対象が見つかりません');
} }
/** /**
* 契約許容市区マスタ CSVエクスポート * CSVエクスポート
*/ */
public function export(Request $request) public function export(Request $request)
{ {
$filename = '契約許容市区マスタ_' . now()->format('YmdHis') . '.csv'; $filename = 'contract_allowable_cities_' . now()->format('Ymd_His') . '.csv';
// 検索条件でデータ取得
$list = ContractAllowableCity::search($request->all()); $list = ContractAllowableCity::search($request->all());
// CSVファイル作成 $headers = [
$file = fopen($filename, 'w+'); 'Content-Type' => 'text/csv',
fwrite($file, "\xEF\xBB\xBF"); // UTF-8 BOM追加 'Content-Disposition' => "attachment; filename=\"$filename\"",
// ヘッダー行
$columns = [
'契約許容市区ID',
'市区ID',
'許容市区名',
'駐輪場ID',
'隣接区フラグ'
]; ];
fputcsv($file, $columns);
// データ行 return new StreamedResponse(function () use ($list) {
$handle = fopen('php://output', 'w');
// ヘッダー
fputcsv($handle, ['契約許容市区ID', '市区ID', '許容市区名', '駐輪場ID', '隣接区フラグ']);
foreach ($list as $item) { foreach ($list as $item) {
fputcsv($file, [ fputcsv($handle, [
$item->contract_allowable_city_id, $item->contract_allowable_city_id,
$item->city_id, $item->city_id,
$item->contract_allowable_city_name, $item->contract_allowable_city_name,
$item->park_id, $item->park_id,
$item->same_district_flag == 0 ? '隣接市' : 'その他', $item->same_district_flag == 0 ? '隣接市' : 'その他'
]); ]);
} }
fclose($file); fclose($handle);
}, 200, $headers);
// ヘッダー設定
$headers = [
"Content-Type" => "text/csv; charset=UTF-8",
"Content-Disposition" => "attachment; filename={$filename}",
];
// ダウンロード後に一時ファイル削除
return response()->download($filename, $filename, $headers)->deleteFileAfterSend(true);
} }
} }

View File

@ -13,9 +13,7 @@ class ContractorController extends Controller
*/ */
public function list(Request $request) public function list(Request $request)
{ {
// ベースクエリを構築
$q = DB::table('regular_contract as rc') $q = DB::table('regular_contract as rc')
->leftJoin('user as u','rc.user_id','=','u.user_id')
->select([ ->select([
'rc.contract_id', 'rc.contract_id',
'rc.contract_qr_id', 'rc.contract_qr_id',
@ -34,12 +32,10 @@ class ContractorController extends Controller
'rc.contract_permission', 'rc.contract_permission',
'rc.contract_manual', 'rc.contract_manual',
'rc.contract_notice', 'rc.contract_notice',
'rc.update_flag',
'p.park_name', 'p.park_name',
'u.user_name', 'u.user_name',
'u.user_phonetic', 'u.user_phonetic',
'u.user_mobile', 'u.user_mobile',
'u.user_seq',
'u.user_homephone', 'u.user_homephone',
'u.user_primemail', 'u.user_primemail',
'u.user_gender', 'u.user_gender',
@ -56,125 +52,37 @@ class ContractorController extends Controller
'u.user_workplace', 'u.user_workplace',
'u.user_school', 'u.user_school',
'u.user_remarks', 'u.user_remarks',
'u.user_tag_serial_64', // 他に必要なカラムもここに追加
'u.user_reduction',
DB::raw('rc.user_securitynum as crime_prevention'),
DB::raw('rc.contract_seal_issue as seal_issue_count'),
DB::raw("CASE rc.enable_months
WHEN 1 THEN '月極(1ヶ月)'
WHEN 3 THEN '3ヶ月'
WHEN 6 THEN '6ヶ月'
WHEN 12 THEN '年'
ELSE CONCAT(rc.enable_months, 'ヶ月') END as ticket_type"),
DB::raw('ps.psection_subject as vehicle_type'),
// 利用者分類のラベルusertype テーブルの subject を取得)
DB::raw('ut.usertype_subject1 as user_category1'),
DB::raw('ut.usertype_subject2 as user_category2'),
DB::raw('ut.usertype_subject3 as user_category3'),
]) ])
->leftJoin('park as p', 'rc.park_id', '=', 'p.park_id') ->leftJoin('park as p', 'rc.park_id', '=', 'p.park_id')
->leftJoin('psection as ps', 'rc.psection_id', '=', 'ps.psection_id') ->leftJoin('user as u', 'rc.user_id', '=', 'u.user_id');
->leftJoin('usertype as ut', 'u.user_categoryid', '=', 'ut.user_categoryid');
// ===== 絞り込み条件 ===== // 検索条件例
if ($request->filled('contract_id')) {
// 駐輪場で絞る(完全一致) $q->where('rc.contract_id', $request->input('contract_id'));
if ($request->filled('park_id')) { }
$q->where('rc.park_id', $request->park_id); if ($request->filled('name')) {
$q->where('u.user_name', 'like', '%' . $request->input('name') . '%');
} }
// 利用者IDで絞る完全一致 // タグ・QR完全一致、空白なら絞り込まない
if ($request->filled('user_id')) { if ($request->filled('tag_qr_flag') && $request->input('tag_qr_flag') !== '') {
$q->where('rc.user_id', $request->user_id); $q->where('rc.tag_qr_flag', $request->input('tag_qr_flag'));
} }
// 利用者分類で絞る(※ select の value を user_categoryid にしているため、user テーブルのカラムで比較) // ソート処理
if ($request->filled('user_category1')) { $sort = $request->input('sort', 'rc.contract_id');
$q->where('u.user_categoryid', $request->user_category1); $sortType = $request->input('sort_type', 'desc');
// カラム名のバリデーション(必要に応じて拡張)
$allowSorts = ['rc.contract_id'];
if (!in_array($sort, $allowSorts)) {
$sort = 'rc.contract_id';
} }
$sortType = ($sortType === 'asc') ? 'asc' : 'desc';
// タグシリアル64進で部分一致検索 $rows = $q->orderBy($sort, $sortType)->paginate(20)->withQueryString();
if ($request->filled('user_tag_serial_64')) {
$val = $request->user_tag_serial_64;
$q->where('u.user_tag_serial_64','like','%'.$val.'%');
}
// 有効期限で絞る(指定日以前を抽出する= <= を使用) return view('admin.contractor.list', compact('rows', 'sort', 'sortType'));
if ($request->filled('contract_periode')) {
$raw = trim($request->contract_periode);
$norm = str_replace('/', '-', $raw); // スラッシュ入力を許容
try {
$target = \Carbon\Carbon::parse($norm)->format('Y-m-d');
// 指定日「以前」を含める
$q->whereDate('rc.contract_periode', '<=', $target);
} catch (\Exception $e) {
// 無効な日付は無視する
}
}
// フリガナで部分一致
if ($request->filled('user_phonetic')) {
$q->where('u.user_phonetic', 'like', '%' . $request->user_phonetic . '%');
}
// 携帯電話で部分一致
if ($request->filled('user_mobile')) {
$q->where('u.user_mobile', 'like', '%' . $request->user_mobile . '%');
}
// メールアドレスで部分一致
if ($request->filled('user_primemail')) {
$q->where('u.user_primemail', 'like', '%' . $request->user_primemail . '%');
}
// 勤務先で部分一致
if ($request->filled('user_workplace')) {
$q->where('u.user_workplace', 'like', '%' . $request->user_workplace . '%');
}
// 学校で部分一致
if ($request->filled('user_school')) {
$q->where('u.user_school', 'like', '%' . $request->user_school . '%');
}
// タグ・QR フラグで絞る(空文字は無視)
if ($request->filled('tag_qr_flag') && $request->tag_qr_flag !== '') {
$q->where('rc.tag_qr_flag', $request->tag_qr_flag);
}
// ===== ソート処理 =====
// 指定があればその列でソート、なければデフォルトで契約IDの昇順
$sort = $request->input('sort'); // null 許容
$sortType = $request->input('sort_type','asc');
$allowSorts = [
'rc.contract_id',
'rc.user_id',
'u.user_name',
'rc.tag_qr_flag',
'p.park_name',
];
if ($sort && in_array($sort, $allowSorts)) {
$sortType = $sortType === 'desc' ? 'desc' : 'asc';
$q->orderBy($sort, $sortType);
} else {
// デフォルトソート
$sort = null;
$sortType = null;
$q->orderBy('rc.contract_id','asc');
}
// ページネーション(クエリ文字列を引き継ぐ)
$rows = $q->paginate(20)->appends($request->query());
// 駐輪場セレクト用データ取得
$parks = DB::table('park')->select('park_id', 'park_name')->orderBy('park_name')->get();
// 利用者分類セレクト用:実際に使用されている分類のみを取得する
$categories = $this->buildCategoryOptions(true);
// ビューに渡す
return view('admin.contractor.list', compact('rows', 'sort', 'sortType', 'parks', 'categories'));
} }
/** /**
@ -182,7 +90,6 @@ class ContractorController extends Controller
*/ */
public function info($id) public function info($id)
{ {
// 指定契約IDの詳細を取得
$contract = DB::table('regular_contract as rc') $contract = DB::table('regular_contract as rc')
->select([ ->select([
'rc.*', 'rc.*',
@ -205,55 +112,4 @@ class ContractorController extends Controller
return view('admin.contractor.info', compact('contract')); return view('admin.contractor.info', compact('contract'));
} }
/**
* 利用者分類選択肢を取得
*
* @param bool $onlyUsed true の場合は regular_contract に出現する分類のみ返す
* @return array [user_categoryid => label, ...]
*/
private function buildCategoryOptions(bool $onlyUsed = false): array
{
if (! $onlyUsed) {
// 全件取得(既存の挙動)
return DB::table('usertype')
->orderBy('user_categoryid', 'asc')
->get()
->mapWithKeys(function ($row) {
$label = collect([
$row->usertype_subject1 ?? '',
$row->usertype_subject2 ?? '',
$row->usertype_subject3 ?? '',
])->filter(fn ($v) => $v !== '')->implode('/');
return [$row->user_categoryid => $label !== '' ? $label : (string) $row->user_categoryid];
})
->toArray();
}
// 実際に使用されている分類のみ取得する(内部結合で user と regular_contract と紐付くもの)
$rows = DB::table('usertype as ut')
->join('user as u', 'u.user_categoryid', '=', 'ut.user_categoryid')
->join('regular_contract as rc', 'rc.user_id', '=', 'u.user_id')
->select(
'ut.user_categoryid',
'ut.usertype_subject1',
'ut.usertype_subject2',
'ut.usertype_subject3'
)
->groupBy('ut.user_categoryid', 'ut.usertype_subject1', 'ut.usertype_subject2', 'ut.usertype_subject3')
->orderBy('ut.user_categoryid', 'asc')
->get();
// ラベルを組み立てて配列で返す
return $rows->mapWithKeys(function ($row) {
$label = collect([
$row->usertype_subject1 ?? '',
$row->usertype_subject2 ?? '',
$row->usertype_subject3 ?? '',
])->filter(fn ($v) => $v !== '')->implode('/');
return [$row->user_categoryid => $label !== '' ? $label : (string) $row->user_categoryid];
})->toArray();
}
} }

View File

@ -13,9 +13,7 @@ class ContractorListController extends Controller
*/ */
public function list(Request $request) public function list(Request $request)
{ {
// ベースクエリを構築
$q = DB::table('regular_contract as rc') $q = DB::table('regular_contract as rc')
->leftJoin('user as u','rc.user_id','=','u.user_id')
->select([ ->select([
'rc.contract_id', 'rc.contract_id',
'rc.contract_qr_id', 'rc.contract_qr_id',
@ -34,14 +32,11 @@ class ContractorListController extends Controller
'rc.contract_permission', 'rc.contract_permission',
'rc.contract_manual', 'rc.contract_manual',
'rc.contract_notice', 'rc.contract_notice',
'rc.update_flag',
'rc.user_securitynum',
'rc.contract_seal_issue',
'p.park_name', 'p.park_name',
// userテーブルの正しいカラム名
'u.user_name', 'u.user_name',
'u.user_phonetic', 'u.user_phonetic',
'u.user_mobile', 'u.user_mobile',
'u.user_seq',
'u.user_homephone', 'u.user_homephone',
'u.user_primemail', 'u.user_primemail',
'u.user_gender', 'u.user_gender',
@ -58,127 +53,37 @@ class ContractorListController extends Controller
'u.user_workplace', 'u.user_workplace',
'u.user_school', 'u.user_school',
'u.user_remarks', 'u.user_remarks',
'u.user_tag_serial_64', // 他に必要なカラムもここに追加
'u.user_reduction',
DB::raw('rc.user_securitynum as crime_prevention'),
DB::raw('rc.contract_seal_issue as seal_issue_count'),
DB::raw("CASE rc.enable_months
WHEN 1 THEN '月極(1ヶ月)'
WHEN 3 THEN '3ヶ月'
WHEN 6 THEN '6ヶ月'
WHEN 12 THEN '年'
ELSE CONCAT(rc.enable_months, 'ヶ月') END as ticket_type"),
DB::raw('ps.psection_subject as vehicle_type'),
// 利用者分類のラベルusertype テーブルの subject を取得)
DB::raw('ut.usertype_subject1 as user_category1'),
DB::raw('ut.usertype_subject2 as user_category2'),
DB::raw('ut.usertype_subject3 as user_category3'),
]) ])
->leftJoin('park as p', 'rc.park_id', '=', 'p.park_id') ->leftJoin('park as p', 'rc.park_id', '=', 'p.park_id')
->leftJoin('psection as ps', 'rc.psection_id', '=', 'ps.psection_id') ->leftJoin('user as u', 'rc.user_id', '=', 'u.user_id');
->leftJoin('usertype as ut', 'u.user_categoryid', '=', 'ut.user_categoryid');
// ===== 絞り込み条件 ===== // 検索条件例
if ($request->filled('contract_id')) {
// 駐輪場で絞る(完全一致) $q->where('rc.contract_id', $request->input('contract_id'));
if ($request->filled('park_id')) { }
$q->where('rc.park_id', $request->park_id); if ($request->filled('name')) {
$q->where('u.user_name', 'like', '%' . $request->input('name') . '%');
} }
// 利用者IDで絞る完全一致 // タグ・QR完全一致、空白なら絞り込まない
if ($request->filled('user_id')) { if ($request->filled('tag_qr_flag') && $request->input('tag_qr_flag') !== '') {
$q->where('rc.user_id', $request->user_id); $q->where('rc.tag_qr_flag', $request->input('tag_qr_flag'));
} }
// 利用者分類で絞る(※ select の value を user_categoryid にしているため、user テーブルのカラムで比較) // ソート処理
if ($request->filled('user_category1')) { $sort = $request->input('sort', 'rc.contract_id');
$q->where('u.user_categoryid', $request->user_category1); $sortType = $request->input('sort_type', 'desc');
// カラム名のバリデーション(必要に応じて拡張)
$allowSorts = ['rc.contract_id'];
if (!in_array($sort, $allowSorts)) {
$sort = 'rc.contract_id';
} }
$sortType = ($sortType === 'asc') ? 'asc' : 'desc';
// タグシリアル64進で部分一致検索 $rows = $q->orderBy($sort, $sortType)->paginate(20)->withQueryString();
if ($request->filled('user_tag_serial_64')) {
$val = $request->user_tag_serial_64;
$q->where('u.user_tag_serial_64','like','%'.$val.'%');
}
// 対象月 return view('admin.contractor_list.list', compact('rows', 'sort', 'sortType'));
$target = $request->input('target_month');
if (in_array($target,['last','this','next','after2'],true)) {
$base = now()->startOfMonth();
$offset = ['last'=>-1,'this'=>0,'next'=>1,'after2'=>2][$target];
$m = $base->copy()->addMonths($offset);
if ($target === 'after2') {
// 2か月後「以降」を抽出該当月の月初以降
$q->whereDate('rc.contract_periode', '>=', $m->toDateString());
} else {
$q->whereYear('rc.contract_periode',$m->year)
->whereMonth('rc.contract_periode',$m->month);
}
}
// フリガナで部分一致
if ($request->filled('user_phonetic')) {
$q->where('u.user_phonetic', 'like', '%' . $request->user_phonetic . '%');
}
// 携帯電話で部分一致
if ($request->filled('user_mobile')) {
$q->where('u.user_mobile', 'like', '%' . $request->user_mobile . '%');
}
// メールアドレスで部分一致
if ($request->filled('user_primemail')) {
$q->where('u.user_primemail', 'like', '%' . $request->user_primemail . '%');
}
// 勤務先で部分一致
if ($request->filled('user_workplace')) {
$q->where('u.user_workplace', 'like', '%' . $request->user_workplace . '%');
}
// 学校で部分一致
if ($request->filled('user_school')) {
$q->where('u.user_school', 'like', '%' . $request->user_school . '%');
}
// タグ・QR フラグで絞る(空文字は無視)
if ($request->filled('tag_qr_flag') && $request->tag_qr_flag !== '') {
$q->where('rc.tag_qr_flag', $request->tag_qr_flag);
}
// ===== ソート処理 =====
// 指定があればその列でソート、なければデフォルトで契約IDの昇順
$sort = $request->input('sort'); // null 許容
$sortType = $request->input('sort_type','asc');
$allowSorts = [
'rc.contract_id',
'rc.user_id',
'u.user_name',
'rc.tag_qr_flag',
'p.park_name',
];
if ($sort && in_array($sort, $allowSorts)) {
$sortType = $sortType === 'desc' ? 'desc' : 'asc';
$q->orderBy($sort, $sortType);
} else {
// デフォルトソート
$sort = null;
$sortType = null;
$q->orderBy('rc.contract_id','asc');
}
// ページネーション(クエリ文字列を引き継ぐ)
$rows = $q->paginate(20)->appends($request->query());
// 駐輪場セレクト用データ取得
$parks = DB::table('park')->select('park_id', 'park_name')->orderBy('park_name')->get();
// 利用者分類セレクト用:実際に使用されている分類のみを取得する
$categories = $this->buildCategoryOptions(true);
// ビューに渡す
return view('admin.contractor_list.list', compact('rows', 'sort', 'sortType', 'parks', 'categories'));
} }
/** /**
@ -186,7 +91,6 @@ class ContractorListController extends Controller
*/ */
public function info($id) public function info($id)
{ {
// 指定契約IDの詳細を取得
$contract = DB::table('regular_contract as rc') $contract = DB::table('regular_contract as rc')
->select([ ->select([
'rc.*', 'rc.*',
@ -207,57 +111,6 @@ class ContractorListController extends Controller
if (!$contract) { abort(404); } if (!$contract) { abort(404); }
return view('admin.contractor_List.info', compact('contract')); return view('admin.contractor.info', compact('contract'));
}
/**
* 利用者分類選択肢を取得
*
* @param bool $onlyUsed true の場合は regular_contract に出現する分類のみ返す
* @return array [user_categoryid => label, ...]
*/
private function buildCategoryOptions(bool $onlyUsed = false): array
{
if (! $onlyUsed) {
// 全件取得(既存の挙動)
return DB::table('usertype')
->orderBy('user_categoryid', 'asc')
->get()
->mapWithKeys(function ($row) {
$label = collect([
$row->usertype_subject1 ?? '',
$row->usertype_subject2 ?? '',
$row->usertype_subject3 ?? '',
])->filter(fn ($v) => $v !== '')->implode('/');
return [$row->user_categoryid => $label !== '' ? $label : (string) $row->user_categoryid];
})
->toArray();
}
// 実際に使用されている分類のみ取得する(内部結合で user と regular_contract と紐付くもの)
$rows = DB::table('usertype as ut')
->join('user as u', 'u.user_categoryid', '=', 'ut.user_categoryid')
->join('regular_contract as rc', 'rc.user_id', '=', 'u.user_id')
->select(
'ut.user_categoryid',
'ut.usertype_subject1',
'ut.usertype_subject2',
'ut.usertype_subject3'
)
->groupBy('ut.user_categoryid', 'ut.usertype_subject1', 'ut.usertype_subject2', 'ut.usertype_subject3')
->orderBy('ut.user_categoryid', 'asc')
->get();
// ラベルを組み立てて配列で返す
return $rows->mapWithKeys(function ($row) {
$label = collect([
$row->usertype_subject1 ?? '',
$row->usertype_subject2 ?? '',
$row->usertype_subject3 ?? '',
])->filter(fn ($v) => $v !== '')->implode('/');
return [$row->user_categoryid => $label !== '' ? $label : (string) $row->user_categoryid];
})->toArray();
} }
} }

View File

@ -8,7 +8,6 @@ use App\Models\Device;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpFoundation\StreamedResponse;
use App\Models\Park;
class DeviceController extends Controller class DeviceController extends Controller
{ {
@ -20,205 +19,124 @@ class DeviceController extends Controller
{ {
$perPage = \App\Utils::item_per_page ?? 20; $perPage = \App\Utils::item_per_page ?? 20;
// リクエストからソート対象と方向を取得(デフォルト: device_id asc
$sort = $request->input('sort', 'device_id');
$sort_type = $request->input('sort_type', 'asc');
// 許可カラムSQLインジェクション対策
$sortable = [
'device_id',
'park_id',
'device_type',
'device_subject',
'device_identifier',
'device_work',
'device_workstart',
'device_replace',
'device_remarks',
'operator_id',
'ope_auth1',
];
if (!in_array($sort, $sortable)) {
$sort = 'device_id';
}
if (!in_array(strtolower($sort_type), ['asc','desc'])) {
$sort_type = 'desc';
}
$list = Device::with('park') $list = Device::with('park')
->orderBy($sort, $sort_type) ->orderBy('device_id', 'desc')
->paginate($perPage) ->paginate($perPage);
->appends([
'sort' => $sort,
'sort_type' => $sort_type,
]);
return view('admin.devices.list', [ return view('admin.devices.list', [
'list' => $list, 'list' => $list,
'sort' => $sort, 'sort' => 'device_id',
'sort_type' => $sort_type, 'sort_type' => 'desc',
]); ]);
} }
/** /**
* 新規登録GET 画面 / POST 保存) * 新規追加: /device/add
*/ */
public function add(Request $request) public function add(Request $request)
{ {
if ($request->isMethod('get')) { if ($request->isMethod('post')) {
return view('admin.devices.add', [ $v = Validator::make($request->all(), $this->rules());
'isEdit' => false, if ($v->fails()) return back()->withErrors($v)->withInput();
'device' => new Device(),
'parks' => Park::all(),
// 初期値Bladeで old() 使うなら省略可) DB::transaction(function () use ($request) {
'device_id' => null, Device::create($request->only([
'park_id' => '', 'park_id','device_type','device_subject','device_identifier',
'device_type' => '', 'device_work','device_workstart','device_replace','device_remarks','operator_id',
'device_subject' => '', ]));
'device_identifier'=> '', });
'device_work' => '',
'device_workstart' => '', return redirect()->route('devices')->with('success', 'デバイスを登録しました。');
'device_replace' => '', }
'device_remarks' => '',
'operator_id' => '', return view('admin.devices.add', [
'device' => new Device(),
'isInfo' => false,
'isEdit' => false,
]); ]);
} }
// 入力値を一旦取得
$data = $request->all();
// --- バリデーション ---
$rules = [
'park_id' => ['required','integer'],
'device_type' => ['required','in:1,2,3'], // 1=サーバー, 2=プリンタ, 3=その他
'device_subject' => ['required','string','max:255'],
'device_identifier' => ['required','string','max:255'],
'device_work' => ['required','in:0,1'], // 1=稼働, 0=停止
'device_workstart' => ['required','date'],
'device_replace' => ['nullable','date'],
'device_remarks' => ['nullable','string','max:255'],
'operator_id' => ['nullable','integer'],
];
$request->validate($rules);
// 保存処理
$device = new Device();
$device->fill($data);
$device->save();
return redirect()->route('devices')->with('success', '登録しました。');
}
/** /**
* 編集GET 画面 / POST 更新) * 編集: /device/edit/{id}
*/ */
public function edit($id, Request $request) public function edit(Request $request, int $id)
{ {
$device = Device::find($id); $device = Device::findOrFail($id);
if (!$device) abort(404);
if ($request->isMethod('get')) { if ($request->isMethod('post')) {
return view('admin.devices.edit', [ $v = Validator::make($request->all(), $this->rules($id));
'isEdit' => true, if ($v->fails()) return back()->withErrors($v)->withInput();
'device' => $device,
'parks' => Park::all(), DB::transaction(function () use ($request, $device) {
]); $device->update($request->only([
'park_id','device_type','device_subject','device_identifier',
'device_work','device_workstart','device_replace','device_remarks','operator_id',
]));
});
return redirect()->route('devices')->with('success', 'デバイスを更新しました。');
} }
// 入力値を一旦取得 return view('admin.devices.edit', [
$data = $request->all(); 'device' => $device,
'isInfo' => false,
// --- バリデーション --- 'isEdit' => true,
$rules = [ ]);
'park_id' => ['required','integer'],
'device_type' => ['required','in:1,2,3'], // 1=サーバー, 2=プリンタ, 3=その他
'device_subject' => ['required','string','max:255'],
'device_identifier' => ['required','string','max:255'],
'device_work' => ['required','in:0,1'], // 1=稼働, 0=停止
'device_workstart' => ['required','date'],
'device_replace' => ['nullable','date'],
'device_remarks' => ['nullable','string','max:255'],
'operator_id' => ['nullable','integer'],
];
$request->validate($rules);
// 保存処理
$device->fill($data);
$device->save();
return redirect()->route('devices')->with('success', '更新しました。');
} }
/** /**
* 詳細: /device/info/{id} * 詳細: /device/info/{id}
*/ */
// public function info(int $id) public function info(int $id)
// { {
// $device = Device::with('park')->findOrFail($id); $device = Device::with('park')->findOrFail($id);
// return view('admin.devices.info', [ return view('admin.devices.info', [
// 'device' => $device, 'device' => $device,
// 'isInfo' => true, 'isInfo' => true,
// 'isEdit' => false, 'isEdit' => false,
// ]); ]);
// } }
/** /**
* 削除(単体 or 複数) * 削除: /device/delete
*/ */
public function delete(Request $request) public function delete(Request $request)
{ {
$ids = []; $ids = $request->input('ids');
$id = $request->input('id');
// 単体削除 if ($id) $ids = [$id];
if ($request->filled('id')) { if (!is_array($ids) || empty($ids)) {
$ids[] = (int) $request->input('id'); return back()->with('error', '削除対象が指定されていません。');
} }
// 複数削除 DB::transaction(function () use ($ids) {
if ($request->filled('ids')) { Device::whereIn('device_id', $ids)->delete();
$ids = array_merge($ids, array_map('intval', (array)$request->input('ids'))); });
return redirect()->route('devices')->with('success', 'デバイスを削除しました。');
} }
$ids = array_unique($ids);
if (!$ids) {
return back()->with('error', '削除対象が選択されていません。');
}
Device::deleteByPk($ids);
return redirect()->route('devices')->with('success', '削除しました。');
}
/** バリデーションルール */ /** バリデーションルール */
private function rules(?int $id = null): array private function rules(?int $id = null): array
{ {
return [ return [
'park_id' => ['required','integer'], // 駐輪場ID 必須 'park_id' => ['nullable','integer'],
'device_type' => ['required','in:1,2,3'], // 1=サーバー, 2=プリンタ, 3=その他 'device_type' => ['required','string','max:255'],
'device_subject' => ['required','string','max:255'], // デバイス名 必須 'device_subject' => ['required','string','max:255'],
'device_identifier' => ['required','string','max:255'], // 識別子 必須 'device_identifier' => ['nullable','string','max:255'],
'device_work' => ['required','in:0,1'], // 1=稼働, 0=停止 'device_work' => ['nullable','string','max:255'],
'device_workstart' => ['required','date'], // 稼働開始日 必須 'device_workstart' => ['nullable','date'],
'device_replace' => ['nullable','date'], // リプレース予約日 任意 'device_replace' => ['nullable','date'],
'device_remarks' => ['nullable','string','max:255'], // 備考 任意 'device_remarks' => ['nullable','string','max:255'],
'operator_id' => ['nullable','integer'], // 任意 'operator_id' => ['nullable','integer'],
]; ];
} }
} }

View File

@ -1,261 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class InformationController extends Controller
{
public function list(Request $request)
{
// パラメータ
$period = $request->input('period', 'month'); // month | all
$type = $request->input('type', 'all'); // task(<99) | hard(>99) | all
$status = $request->input('status', 'untreated'); // untreated(=1) | inprogress(=2) | done(=3) | all
$q = 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')
// オペレータマスタ(テーブル・カラム名は環境に合わせて調整)
->leftJoin('ope as o', 'oq.operator_id', '=', 'o.ope_id')
->select(
'oq.que_id','oq.que_class','oq.user_id',
DB::raw('u.user_name as user_name'),
'oq.contract_id','oq.park_id',
DB::raw('p.park_name as park_name'),
'oq.que_comment','oq.que_status','oq.que_status_comment',
'oq.work_instructions','oq.created_at','oq.updated_at','oq.operator_id',
DB::raw('o.ope_name as operator_name')
);
// 期間: 登録日ベース最新1ヵ月 or 全期間)
if ($period === 'month') {
$q->where('oq.created_at', '>=', now()->subMonth());
}
// 種別: que_class
if ($type === 'task') {
$q->where('oq.que_class', '<', 99);
} elseif ($type === 'hard') {
$q->where('oq.que_class', '>', 99);
} // all は絞り込みなし
// ステータス: que_status
if ($status === 'untreated') {
$q->where('oq.que_status', 1);
} elseif ($status === 'inprogress') {
$q->where('oq.que_status', 2);
} elseif ($status === 'done') {
$q->where('oq.que_status', 3);
} // all は絞り込みなし
$jobs = $q->orderBy('oq.que_id')->paginate(20)->appends($request->query());
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)
{
$request->validate([
'ids' => 'required|array',
'action' => 'required|in:inprogress,done',
]);
$new = $request->action === 'inprogress' ? 2 : 3;
DB::table('operator_que')
->whereIn('que_id', $request->ids)
->update([
'que_status' => $new,
'updated_at' => now(),
]);
return back()->with('success', '選択したキューのステータスを更新しました。');
}
}

View File

@ -1,226 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\InvSetting;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class InvSettingController extends Controller
{
/**
* 登録フォーム表示
*/
public function form(Request $request)
{
$row = InvSetting::first();
$zip1 = $zip2 = $tel1 = $tel2 = $tel3 = $fax1 = $fax2 = $fax3 = '';
if ($row) {
// 郵便番号(そのままハイフン分割)
if (!empty($row->zipcode) && str_contains($row->zipcode, '-')) {
[$zip1, $zip2] = explode('-', $row->zipcode);
}
// 電話番号:数字以外を除去 → 2桁+4桁+4桁 に分割
if (!empty($row->tel_num)) {
$tel = preg_replace('/\D/', '', $row->tel_num); // 数字以外を除去
$tel1 = substr($tel, 0, 2);
$tel2 = substr($tel, 2, 4);
$tel3 = substr($tel, 6, 4);
}
// FAX番号同じく 2桁+4桁+4桁
if (!empty($row->fax_num)) {
$fax = preg_replace('/\D/', '', $row->fax_num);
$fax1 = substr($fax, 0, 2);
$fax2 = substr($fax, 2, 4);
$fax3 = substr($fax, 6, 4);
}
}
return view('admin.invsettings._form', compact(
'row', 'zip1', 'zip2', 'tel1', 'tel2', 'tel3', 'fax1', 'fax2', 'fax3'
));
}
/**
* 登録・更新処理
*/
public function save(Request $request)
{
// ▼ バリデーションルール
$rules = [
't_number' => 'required|string|max:20',
't_name' => 'required|string|max:50',
'zip1' => 'required|digits:3',
'zip2' => 'required|digits:4',
'adrs' => 'required|string|max:100',
'bldg' => 'nullable|string|max:80',
'tel1' => 'nullable|digits_between:2,4',
'tel2' => 'nullable|digits_between:2,4',
'tel3' => 'nullable|digits_between:3,4',
'fax1' => 'nullable|digits_between:2,4',
'fax2' => 'nullable|digits_between:2,4',
'fax3' => 'nullable|digits_between:3,4',
'company_image_path' => 'nullable|string|max:255',
];
// ▼ カスタム日本語メッセージ
$messages = [
't_number.required' => '適格請求書発行事業者番号を入力してください。',
't_number.max' => '適格請求書発行事業者番号は20文字以内で入力してください。',
't_name.required' => '適格事業者名を入力してください。',
't_name.max' => '適格事業者名は50文字以内で入力してください。',
'zip1.required' => '郵便番号(前半)を入力してください。',
'zip1.digits' => '郵便番号前半は3桁で入力してください。',
'zip2.required' => '郵便番号(後半)を入力してください。',
'zip2.digits' => '郵便番号後半は4桁で入力してください。',
'adrs.required' => '表示住所を入力してください。',
'adrs.max' => '表示住所は100文字以内で入力してください。',
'tel1.digits_between' => '電話番号1は2桁から4桁で入力してください。',
'tel2.digits_between' => '電話番号2は2桁から4桁で入力してください。',
'tel3.digits_between' => '電話番号3は3桁から4桁で入力してください。',
'fax1.digits_between' => 'FAX番号1は2桁から4桁で入力してください。',
'fax2.digits_between' => 'FAX番号2は2桁から4桁で入力してください。',
'fax3.digits_between' => 'FAX番号3は3桁から4桁で入力してください。',
];
// ▼ バリデーション実行
$request->validate($rules, $messages);
// ▼ データ整形
$zipcode = $request->zip1 . '-' . $request->zip2;
$tel = implode('-', array_filter([$request->tel1, $request->tel2, $request->tel3]));
$fax = implode('-', array_filter([$request->fax1, $request->fax2, $request->fax3]));
// ▼ 既存レコードを取得1レコード運用
$row = InvSetting::first();
// ▼ 画像パスを設定
$imagePath = $request->company_image_path;
// ▼ フォームで新たにファイルを送信した場合のみ再保存(保険的処理)
if ($request->hasFile('company_image')) {
if ($imagePath && Storage::disk('public')->exists($imagePath)) {
Storage::disk('public')->delete($imagePath);
}
$imagePath = $request->file('company_image')->store('inv', 'public');
}
// ▼ レコードを新規作成 or 更新
if ($row) {
$row->update([
't_number' => $request->t_number,
't_name' => $request->t_name,
'zipcode' => $zipcode,
'adrs' => $request->adrs,
'bldg' => $request->bldg,
'tel_num' => $tel,
'fax_num' => $fax,
'company_image_path' => $imagePath, // ← hiddenの値 or 新規アップロード結果を保存
]);
} else {
InvSetting::create([
't_number' => $request->t_number,
't_name' => $request->t_name,
'zipcode' => $zipcode,
'adrs' => $request->adrs,
'bldg' => $request->bldg,
'tel_num' => $tel,
'fax_num' => $fax,
'company_image_path' => $imagePath,
]);
}
return back()->with('success', 'インボイス設定を登録しました。');
}
/**
* 社判画像アップロードAJAX用
*/
// public function upload(Request $request)
// {
// // ファイルがアップロードされているか確認
// if ($request->hasFile('company_image_file')) {
// // 拡張子チェック & バリデーション
// $request->validate([
// 'company_image_file' => 'required|image|mimes:png,jpg,jpeg|max:2048',
// ], [
// 'company_image_file.image' => '画像ファイルを選択してください。',
// 'company_image_file.mimes' => 'アップロード可能な形式は png, jpg, jpeg のみです。',
// 'company_image_file.max' => 'ファイルサイズは2MB以下にしてください。',
// ]);
// // ファイル保存public/storage/inv に格納)
// $path = $request->file('company_image_file')->store('inv', 'public');
// // ファイル名を抽出
// $fileName = basename($path);
// // JSONで返却JSが受け取る
// return response()->json([
// 'file_name' => $fileName,
// 'path' => $path,
// ]);
// }
// // ファイル未選択時
// return response()->json([
// 'error' => 'ファイルが選択されていません。'
// ], 400);
// }
public function upload(Request $request)
{
// ファイルがアップロードされているか確認
if ($request->hasFile('company_image_file')) {
// 拡張子チェック & バリデーション
$request->validate([
'company_image_file' => 'required|image|mimes:png,jpg,jpeg|max:2048',
], [
'company_image_file.image' => '画像ファイルを選択してください。',
'company_image_file.mimes' => 'アップロード可能な形式は png, jpg, jpeg のみです。',
'company_image_file.max' => 'ファイルサイズは2MB以下にしてください。',
]);
// ファイルオブジェクト取得
$file = $request->file('company_image_file');
// 元のファイル名company_logo.png
$originalName = $file->getClientOriginalName();
// 保存用に、ファイル名の重複を避けるためにユニーク名を生成(推奨)
$fileName = $originalName;
// public/storage/inv に保存
$path = $file->storeAs('inv', $fileName, 'public');
// JSONで返却JS側で表示用ファイル名を使う
return response()->json([
'file_name' => $originalName, // ユーザーが見えるファイル名
'stored_as' => $fileName, // 実際に保存されたファイル名
'path' => $path,
]);
}
// ファイル未選択時
return response()->json([
'error' => 'ファイルが選択されていません。'
], 400);
}
}

View File

@ -12,77 +12,45 @@ use Illuminate\Support\Facades\DB;
class JurisdictionParkingController extends Controller class JurisdictionParkingController extends Controller
{ {
public function list(Request $request) public function list(Request $request)
{ {
$list = JurisdictionParking::query()->paginate(20);
$sort = $request->input('sort', 'jurisdiction_parking_id'); return view('admin.jurisdiction_parkings.list', compact('list'));
$sort_type = $request->input('sort_type', 'asc'); }
$list = JurisdictionParking::orderBy($sort, $sort_type)->paginate(20);
return view('admin.jurisdiction_parkings.list', compact('list', 'sort', 'sort_type'));
}
public function add(Request $request) public function add(Request $request)
{ {
if ($request->isMethod('post')) { if ($request->isMethod('post')) {
$validated = $request->validate([ $validated = $request->validate([
'jurisdiction_parking_name' => [ 'jurisdiction_parking_name' => 'required|string|max:255',
'required', 'operator_id' => 'nullable|integer',
'string', 'park_id' => 'nullable|integer',
'max:20',
],
'ope_id' => [
'required',
],
'park_id' => [
'required',
],
'operator_id' => [
'nullable',
'integer',
],
]); ]);
JurisdictionParking::create($validated); JurisdictionParking::create($validated);
return redirect()->route('jurisdiction_parkings')->with('success', '登録しました');
return redirect()->route('jurisdiction_parkings')
->with('success', '登録しました。');
} }
$parks = Park::pluck('park_name', 'park_id'); $parks = Park::pluck('park_name', 'park_id');
$opes = Ope::pluck('ope_name', 'ope_id'); $operators = Ope::pluck('ope_name', 'ope_id');
return view('admin.jurisdiction_parkings.add', compact('parks', 'opes')); return view('admin.jurisdiction_parkings.add', compact('parks', 'operators'));
} }
public function edit(Request $request, $id) public function edit(Request $request, $jurisdiction_parking_id)
{ {
$record = JurisdictionParking::findOrFail($id); $record = JurisdictionParking::findOrFail($jurisdiction_parking_id);
if ($request->isMethod('post')) { if ($request->isMethod('post')) {
$validated = $request->validate([ $validated = $request->validate([
'jurisdiction_parking_name' => [ 'jurisdiction_parking_name' => 'required|string|max:255',
'required', 'ope_id' => 'nullable|integer',
'string', 'park_id' => 'nullable|integer',
'max:20', 'operator_id' => 'nullable|integer',
],
'ope_id' => [
'required',
],
'park_id' => [
'required',
],
'operator_id' => [
'nullable',
'integer',
],
]); ]);
$record->update($validated); $record->update($validated);
return redirect()->route('jurisdiction_parkings')->with('success', '更新しました');
return redirect()->route('jurisdiction_parkings')
->with('success', '更新しました。');
} }
$parks = Park::pluck('park_name', 'park_id'); $parks = Park::pluck('park_name', 'park_id');
@ -91,25 +59,14 @@ class JurisdictionParkingController extends Controller
return view('admin.jurisdiction_parkings.edit', compact('record', 'parks', 'opes')); return view('admin.jurisdiction_parkings.edit', compact('record', 'parks', 'opes'));
} }
public function delete(Request $request) public function delete(Request $request)
{ {
$request->validate([ if ($request->has('pk')) {
'pk' => 'required', JurisdictionParking::destroy($request->input('pk'));
'pk.*' => 'integer', // 各要素が整数であることを確認 return redirect()->route('jurisdiction_parkings')->with('success', '削除しました');
]);
$ids = (array) $request->input('pk'); // 配列として取得
$deleted = JurisdictionParking::destroy($ids);
if ($deleted > 0) {
return redirect()->route('jurisdiction_parkings')
->with('success', '削除しました。');
} else {
return redirect()->route('jurisdiction_parkings')
->with('error', '削除に失敗しました。');
} }
return redirect()->route('jurisdiction_parkings')->with('error', '削除対象が見つかりません');
} }
public function info(Request $request, $jurisdiction_parking_id) public function info(Request $request, $jurisdiction_parking_id)

View File

@ -1,146 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\MailTemplate;
class MailTemplateController extends Controller
{
/**
* 一覧表示
*/
public function list(Request $request)
{
if ($request->input('action') === 'reset') {
return redirect()->route('mail_templates');
}
$allowedSorts = [
'mail_template_id', 'pg_id', 'internal_id', 'mgr_cc_flag',
'bcc_adrs', 'use_flag', 'memo', 'subject', 'text',
'created_at', 'updated_at', 'operator_id'
];
$sort = $request->input('sort', 'mail_template_id');
$sort_type = $request->input('sort_type', 'asc');
if (!in_array($sort, $allowedSorts)) {
$sort = 'mail_template_id';
}
if (!in_array($sort_type, ['asc', 'desc'])) {
$sort_type = 'desc';
}
$query = MailTemplate::query();
// === 絞り込み ===
$mail_template_id = $request->input('mail_template_id');
$pg_id = $request->input('pg_id');
$mgr_cc_flag = $request->input('mgr_cc_flag');
$use_flag = $request->input('use_flag');
$subject = $request->input('subject');
if ($mail_template_id) {
$query->where('mail_template_id', $mail_template_id);
}
if ($pg_id) {
$query->where('pg_id', $pg_id);
}
if ($mgr_cc_flag !== null && $mgr_cc_flag !== '') {
$query->where('mgr_cc_flag', $mgr_cc_flag);
}
if ($use_flag !== null && $use_flag !== '') {
$query->where('use_flag', $use_flag);
}
if ($subject) {
$query->where('subject', 'LIKE', "%{$subject}%");
}
$templates = $query->orderBy($sort, $sort_type)->paginate(20);
return view('admin.mail_templates.list', compact(
'templates', 'sort', 'sort_type',
'mail_template_id', 'pg_id', 'mgr_cc_flag', 'use_flag', 'subject'
));
}
public function add(Request $request)
{
if ($request->isMethod('post')) {
$data = $this->validateTemplate($request);
$data['operator_id'] = optional(\Auth::user())->ope_id ?? null;
MailTemplate::create($data);
return redirect()->route('mail_templates')
->with('success', '登録しました。');
}
return view('admin.mail_templates.add', [
'mailTemplate' => new MailTemplate(),
'isEdit' => false,
]);
}
/**
* 編集
*/
public function edit(int $id, Request $request)
{
$mailTemplate = MailTemplate::findOrFail($id);
if ($request->isMethod('post')) {
$data = $this->validateTemplate($request);
$data['operator_id'] = optional(\Auth::user())->ope_id ?? null;
$mailTemplate->update($data);
return redirect()->route('mail_templates')
->with('success', '更新しました。');
}
return view('admin.mail_templates.edit', [
'mailTemplate' => $mailTemplate,
'isEdit' => true,
]);
}
/**
* 削除
*/
public function delete(Request $request)
{
$pk = $request->input('pk', []);
// 配列に統一
$ids = is_array($pk) ? $pk : [$pk];
$ids = array_values(array_filter($ids, fn($v) => preg_match('/^\d+$/', (string) $v)));
if (empty($ids)) {
return redirect()->route('mail_templates')->with('error', '削除対象が選択されていません。');
}
MailTemplate::whereIn('mail_template_id', $ids)->delete();
return redirect()->route('mail_templates')->with('success', '削除しました。');
}
/**
* バリデーション共通化
*/
private function validateTemplate(Request $request)
{
return $request->validate([
'pg_id' => 'required|integer',
'internal_id' => 'required|integer',
'mgr_cc_flag' => 'required|boolean',
'bcc_adrs' => 'nullable|string|max:255',
'use_flag' => 'required|boolean',
'memo' => 'nullable|string|max:255',
'subject' => 'required|string|max:255',
'text' => 'required|string',
]);
}
}

View File

@ -31,38 +31,39 @@ class ManagerController extends Controller
return view('admin.managers.list', compact('list','sort','sort_type')); return view('admin.managers.list', compact('list','sort','sort_type'));
} }
/** 新規登録画面・登録処理 */ /** 新規GET:画面表示 / POST:登録) */
public function add(Request $request) public function add(Request $request)
{ {
if ($request->isMethod('post')) { if ($request->isMethod('post')) {
$validated = $this->validated($request); $data = $this->validated($request);
Manager::create($validated);
$manager = Manager::create($data);
return redirect() return redirect()
->route('managers') ->route('managers_info', ['manager_id' => $manager->manager_id])
->with('success', '登録しました。'); ->with('success', '登録しました。');
} }
return view('admin.managers.add', $this->viewVars()); // 画面に渡す初期値
$view = $this->viewVars();
return view('admin.managers.add', $view);
} }
/** 編集GET:画面表示 / POST:更新) */ /** 編集GET:画面表示 / POST:更新) */
public function edit(Request $request, $id) public function edit(Request $request, $manager_id)
{ {
$manager = Manager::findOrFail($id); $manager = Manager::findOrFail($manager_id);
if ($request->isMethod('post')) { if ($request->isMethod('post')) {
$validated = $this->validated($request); $data = $this->validated($request);
$manager->fill($data)->save();
$manager->update($validated);
return redirect() return redirect()
->route('managers') ->route('managers_info', ['manager_id' => $manager->manager_id])
->with('success', '更新されました。'); ->with('success', '更新ました。');
} }
return view('admin.managers.edit', $this->viewVars($manager)); $view = $this->viewVars($manager);
return view('admin.managers.edit', $view);
} }
/** 詳細(閲覧) */ /** 詳細(閲覧) */
@ -77,19 +78,13 @@ class ManagerController extends Controller
public function delete(Request $request) public function delete(Request $request)
{ {
$ids = (array) $request->input('pk', []); $ids = (array) $request->input('pk', []);
if (!$ids) { if (!$ids) return back()->with('error', '削除対象が選択されていません。');
return back()->with('error', '削除対象が選択されていません。');
}
DB::transaction(fn() => Manager::whereIn('manager_id', $ids)->delete()); DB::transaction(fn() => Manager::whereIn('manager_id', $ids)->delete());
// 一覧画面へリダイレクト + 成功メッセージ return back()->with('success', '削除しました。');
return redirect()
->route('managers')
->with('success', '削除しました。');
} }
/** CSV出力 */ /** CSV出力 */
public function export(): StreamedResponse public function export(): StreamedResponse
{ {
@ -172,32 +167,22 @@ class ManagerController extends Controller
} }
} }
/** バリデーション + 前処理 */ /* ======================== private helpers ======================== */
/** バリデーション */
private function validated(Request $request): array private function validated(Request $request): array
{ {
// 電話番号を全角に変換(半角入力があっても自動で全角に揃える)
$request->merge([
'manager_tel' => mb_convert_kana($request->input('manager_tel'), 'N') // 半角数字→全角数字
]);
// select の未選択 "" を null に補正
foreach (['manager_device2'] as $f) {
if ($request->input($f) === "") {
$request->merge([$f => null]);
}
}
return $request->validate([ return $request->validate([
'manager_name' => ['required','string','max:32'], 'manager_name' => ['required','string','max:255'],
'manager_type' => ['required','string','max:10'], 'manager_type' => ['nullable','string','max:255'],
'manager_parkid' => ['required','integer','exists:park,park_id'], 'manager_parkid' => ['nullable','integer','exists:park,park_id'], // テーブル名に合わせて
'manager_device1' => ['required','integer','exists:device,device_id'], 'manager_device1' => ['nullable','integer','exists:device,device_id'],
'manager_device2' => ['nullable','integer','exists:device,device_id'], 'manager_device2' => ['nullable','integer','exists:device,device_id'],
'manager_mail' => ['nullable','email','max:128'], 'manager_mail' => ['nullable','email','max:255'],
'manager_tel' => ['required','regex:/^[-]+$/u','max:13'], // 全角数字のみ 'manager_tel' => ['nullable','string','max:255'],
'manager_alert1' => ['nullable','boolean'], 'manager_alert1' => ['nullable','boolean'],
'manager_alert2' => ['nullable','boolean'], 'manager_alert2' => ['nullable','boolean'],
'manager_quit_flag' => ['required','in:0,1'], 'manager_quit_flag' => ['nullable','boolean'],
'manager_quitday' => ['nullable','date'], 'manager_quitday' => ['nullable','date'],
], [], [ ], [], [
'manager_name' => '駐輪場管理者名', 'manager_name' => '駐輪場管理者名',
@ -214,7 +199,6 @@ class ManagerController extends Controller
]); ]);
} }
/** 画面に渡す変数を作る_form.blade.php が個別変数を参照するため) */ /** 画面に渡す変数を作る_form.blade.php が個別変数を参照するため) */
private function viewVars(?Manager $m = null): array private function viewVars(?Manager $m = null): array
{ {

View File

@ -0,0 +1,119 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\NeighborStation;
class NeighborStationController extends Controller
{
// 一覧表示
public function list(Request $request)
{
$sort = $request->input('sort', 'station_id');
$sort_type = $request->input('sort_type', 'asc');
$allowedSorts = ['station_id', 'park_id', 'station_neighbor_station', 'station_name_ruby', 'station_route_name'];
if (!in_array($sort, $allowedSorts)) {
$sort = 'station_id';
}
if (!in_array($sort_type, ['asc', 'desc'])) {
$sort_type = 'asc';
}
$stations = NeighborStation::select([
'station_id',
'station_neighbor_station',
'station_name_ruby',
'station_route_name',
// 'station_latitude',
// 'station_longitude',
'park_id'
])->orderBy($sort, $sort_type)->paginate(20);
return view('admin.neighbor_stations.list', compact('stations', 'sort', 'sort_type'));
}
// 新規登録画面と登録処理
public function add(Request $request)
{
if ($request->isMethod('post')) {
$validated = $request->validate([
'station_neighbor_station' => 'required|string|max:255',
'station_name_ruby' => 'nullable|string|max:255',
'station_route_name' => 'nullable|string|max:255',
'park_id' => 'nullable|integer',
'operator_id' => 'nullable|integer',
]);
NeighborStation::create($validated);
return redirect()->route('neighbor_stations')->with('success', '近傍駅が登録されました');
}
return view('admin.neighbor_stations.add');
}
// 編集画面・更新処理
public function edit(Request $request, $id)
{
$station = NeighborStation::findOrFail($id);
if ($request->isMethod('post')) {
$validated = $request->validate([
'station_neighbor_station' => 'required|string|max:255',
'station_name_ruby' => 'nullable|string|max:255',
'station_route_name' => 'nullable|string|max:255',
'park_id' => 'nullable|integer',
'operator_id' => 'nullable|integer',
]);
$station->update($validated);
return redirect()->route('neighbor_stations')->with('success', '更新しました');
}
return view('admin.neighbor_stations.edit', compact('station'));
}
// 詳細表示
public function info($id)
{
$station = NeighborStation::findOrFail($id);
return view('admin.neighbor_stations.info', compact('station'));
}
// 削除処理
public function delete(Request $request)
{
$ids = $request->input('pk'); // ← 接收复数 checkbox 名称 pk[]
if (!empty($ids)) {
NeighborStation::destroy($ids); // 一次性删除多个
return redirect()->route('neighbor_stations')->with('success', '削除しました');
}
return redirect()->route('neighbor_stations')->with('error', '削除対象が見つかりません');
}
// CSVインポート
public function import(Request $request)
{
// TODO: 実装
return redirect()->route('neighbor_stations')->with('info', 'CSVインポートは未実装です');
}
// CSVエクスポート
public function export()
{
// TODO: 実装
return response()->streamDownload(function () {
echo "id,station_neighbor_station,station_name_ruby,station_route_name,park_id,operator_id\n";
foreach (NeighborStation::all() as $station) {
echo "{$station->id},{$station->station_neighbor_station},{$station->station_name_ruby},{$station->station_route_name},{$station->park_id},{$station->operator_id}\n";
}
}, 'neighbor_stations.csv');
}
}

View File

@ -61,29 +61,11 @@ class NewsController extends Controller
if ($request->filled('from')) { $q->where('open_datetime','>=',$request->input('from')); } if ($request->filled('from')) { $q->where('open_datetime','>=',$request->input('from')); }
if ($request->filled('to')) { $q->where('open_datetime','<=',$request->input('to')); } if ($request->filled('to')) { $q->where('open_datetime','<=',$request->input('to')); }
// {追加} 並び替え(ホワイトリスト) // 並び順:公開日時の降順 → 主キー降順
$sort = (string)$request->query('sort', ''); $rows = $q->orderByDesc('open_datetime')
$dir = strtolower((string)$request->query('dir', 'desc')); ->orderByDesc($this->pk)
$dir = in_array($dir, ['asc','desc'], true) ? $dir : 'desc'; ->paginate(20)
->withQueryString();
// 画面キー → 実カラム
$sortable = [
'id' => $this->pk, // ニュースID
'news' => 'news', // ニュース内容
'open_datetime' => 'open_datetime', // 公開日時
'mode' => 'mode', // 表示モード
];
if (isset($sortable[$sort])) {
$q->orderBy($sortable[$sort], $dir);
} else {
// 既定:公開日時降順 → 主キー降順
$q->orderByDesc('open_datetime')
->orderByDesc($this->pk);
}
// ページング(現在のクエリを維持)
$rows = $q->paginate(20)->appends($request->except('page'));
return view('admin.news.list', compact('rows')); return view('admin.news.list', compact('rows'));
} }
@ -94,29 +76,21 @@ class NewsController extends Controller
public function add(Request $request) public function add(Request $request)
{ {
if ($request->isMethod('post')) { if ($request->isMethod('post')) {
$messages = [ // 入力チェック
'required' => ':attribute は、必ず入力してください。',
'open_datetime.date_format' => '公開日時は :format 形式YYYY-MM-DD HH:MM:SSで入力してください。',
];
$attributes = [
'news' => 'ニュース内容',
'open_datetime' => '公開日時',
'mode' => '表示モード',
];
$v = $request->validate([ $v = $request->validate([
'news' => 'required|string', 'news' => 'required|string',
'open_datetime' => 'required|date_format:Y-m-d H:i:s', 'open_datetime' => 'nullable|date_format:Y-m-d H:i:s',
'link_url' => 'nullable|string|max:255', 'link_url' => 'nullable|string|max:255',
'image1_filename' => 'nullable|string|max:255', 'image1_filename' => 'nullable|string|max:255',
'image2_filename' => 'nullable|string|max:255', 'image2_filename' => 'nullable|string|max:255',
'mode' => 'required|integer|min:0|max:9', 'mode' => 'required|integer|min:0|max:9',
], $messages, $attributes); ]);
// 登録 // 登録
$now = now(); $now = now();
DB::table($this->table)->insert([ DB::table($this->table)->insert([
'news' => $v['news'], 'news' => $v['news'],
'open_datetime' => $v['open_datetime'], 'open_datetime' => $v['open_datetime'] ?? null,
'link_url' => $v['link_url'] ?? null, 'link_url' => $v['link_url'] ?? null,
'image1_filename' => $v['image1_filename'] ?? null, 'image1_filename' => $v['image1_filename'] ?? null,
'image2_filename' => $v['image2_filename'] ?? null, 'image2_filename' => $v['image2_filename'] ?? null,
@ -144,28 +118,20 @@ class NewsController extends Controller
if (!$news) { abort(404); } if (!$news) { abort(404); }
if ($request->isMethod('post')) { if ($request->isMethod('post')) {
$messages = [ // 入力チェック
'required' => ':attribute は、必ず入力してください。',
'open_datetime.date_format' => '公開日時は :format 形式YYYY-MM-DDで入力してください。',
];
$attributes = [
'news' => 'ニュース内容',
'open_datetime' => '公開日時',
'mode' => '表示モード',
];
$v = $request->validate([ $v = $request->validate([
'news' => 'required|string', 'news' => 'required|string',
'open_datetime' => 'required|date_format:Y-m-d', 'open_datetime' => 'nullable|date_format:Y-m-d H:i:s',
'link_url' => 'nullable|string|max:255', 'link_url' => 'nullable|string|max:255',
'image1_filename' => 'nullable|string|max:255', 'image1_filename' => 'nullable|string|max:255',
'image2_filename' => 'nullable|string|max:255', 'image2_filename' => 'nullable|string|max:255',
'mode' => 'required|integer|min:0|max:9', 'mode' => 'required|integer|min:0|max:9',
], $messages, $attributes); ]);
// 更新 // 更新
DB::table($this->table)->where($this->pk, $id)->update([ DB::table($this->table)->where($this->pk, $id)->update([
'news' => $v['news'], 'news' => $v['news'],
'open_datetime' => $v['open_datetime'] . ' 00:00:00', 'open_datetime' => $v['open_datetime'] ?? null,
'link_url' => $v['link_url'] ?? null, 'link_url' => $v['link_url'] ?? null,
'image1_filename' => $v['image1_filename'] ?? null, 'image1_filename' => $v['image1_filename'] ?? null,
'image2_filename' => $v['image2_filename'] ?? null, 'image2_filename' => $v['image2_filename'] ?? null,

View File

@ -1,5 +1,4 @@
<?php <?php
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
@ -8,9 +7,6 @@ use App\Models\Ope;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpFoundation\StreamedResponse;
use App\Models\Feature;
use App\Models\Permission;
use App\Models\OpePermission;
class OpeController extends Controller class OpeController extends Controller
{ {
@ -22,7 +18,7 @@ class OpeController extends Controller
$inputs = [ $inputs = [
'isMethodPost' => $request->isMethod('post'), 'isMethodPost' => $request->isMethod('post'),
'sort' => $request->input('sort', 'ope_id'), 'sort' => $request->input('sort', 'ope_id'),
'sort_type' => $request->input('sort_type', 'asc'), 'sort_type' => $request->input('sort_type', 'desc'),
'isExport' => false, 'isExport' => false,
]; ];
@ -31,6 +27,7 @@ class OpeController extends Controller
$sort = $inputs['sort']; $sort = $inputs['sort'];
$sort_type = $inputs['sort_type']; $sort_type = $inputs['sort_type'];
return view('admin.opes.list', compact('list', 'sort', 'sort_type')); return view('admin.opes.list', compact('list', 'sort', 'sort_type'));
} }
@ -39,222 +36,109 @@ class OpeController extends Controller
*/ */
public function add(Request $request) 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')) { if ($request->isMethod('get')) {
// add.blade.php は include する _form が期待する変数名を使う
return view('admin.opes.add', [ return view('admin.opes.add', [
'isEdit' => false, 'isEdit' => 0,
'record' => new Ope(), 'isInfo' => 0,
// 初期値存在しなくてもOKだが、Notice 防止のために入れておく)
'ope_id' => null, 'ope_id' => null,
'ope_name' => '', 'ope_name' => '',
'ope_type' => '', 'ope_type' => '',
'ope_mail' => '', 'ope_mail' => '',
'ope_phone' => '', 'ope_phone'=> '',
// 以下はフォームで参照される可能性のあるキーを空で用意
'ope_sendalart_que1' => 0, 'ope_sendalart_que2' => 0, 'ope_sendalart_que3' => 0, 'ope_sendalart_que1' => 0, 'ope_sendalart_que2' => 0, 'ope_sendalart_que3' => 0,
'ope_sendalart_que4' => 0, 'ope_sendalart_que5' => 0, 'ope_sendalart_que6' => 0, 'ope_sendalart_que4' => 0, 'ope_sendalart_que5' => 0, 'ope_sendalart_que6' => 0,
'ope_sendalart_que7' => 0, 'ope_sendalart_que8' => 0, 'ope_sendalart_que9' => 0, 'ope_sendalart_que7' => 0, 'ope_sendalart_que8' => 0, 'ope_sendalart_que9' => 0,
'ope_sendalart_que10' => 0, 'ope_sendalart_que11' => 0, 'ope_sendalart_que12' => 0, 'ope_sendalart_que10'=> 0, 'ope_sendalart_que11'=> 0, 'ope_sendalart_que12'=> 0,
'ope_sendalart_que13' => 0, 'ope_sendalart_que13'=> 0,
'ope_auth1' => '', 'ope_auth2' => '', 'ope_auth3' => '', 'ope_auth4' => '', 'ope_auth1' => '', 'ope_auth2' => '', 'ope_auth3' => '', 'ope_auth4' => '',
'ope_quit_flag' => 0, 'ope_quitday' => '', 'ope_quit_flag' => 0, 'ope_quitday' => '',
// ▼追加権限設定UI用
'features' => $features,
'permissions' => $permissions,
'selectedFeatureId' => old('feature_id', null),
]); ]);
} }
// 入力値を一旦取得
$data = $request->all();
// --- バリデーション ---
$rules = [ $rules = [
'login_id' => 'required|string|max:255|unique:ope,login_id',
'ope_name' => 'required|string|max:255', 'ope_name' => 'required|string|max:255',
'ope_type' => 'required|string|max:50', 'ope_type' => 'required|string|max:50',
'ope_mail' => [ 'ope_mail' => 'nullable|email|max:255',
'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)) {
$fail("無効なメールアドレス形式です: {$email}");
}
}
}
],
'ope_phone' => 'nullable|string|max:50', 'ope_phone' => 'nullable|string|max:50',
'password' => 'required|string|min:8|confirmed',
]; ];
$this->validate($request, $rules);
$request->validate($rules);
// --- 保存用にメールを ; 区切りに統一 ---
$emails = array_filter(array_map('trim', explode(';', str_replace(',', ';', $data['ope_mail']))));
$data['ope_mail'] = implode(';', $emails);
// 保存処理
$ope = new Ope(); $ope = new Ope();
$ope->fill($data); $ope->fill($request->only($ope->getFillable()));
$ope->save(); $ope->save();
return redirect()->route('opes')->with('success', '登録しました。'); return redirect()->route('opes')->with('success', 'オペレータを登録しました。');
} }
/** /**
* 編集GET 画面 / POST 更新) * 編集GET 画面 / POST 更新)
* ※権限(自治体×機能×操作)も同画面で設定する
*/ */
public function edit($id, Request $request) public function edit($id, Request $request)
{ {
$ope = Ope::getByPk($id); $ope = Ope::getByPk($id);
if (!$ope) abort(404); if (!$ope) abort(404);
// ※機能(画面)一覧を取得(プルダウン用)
$features = Feature::query()
->orderBy('id')
->get(['id', 'name']);
// ※操作権限一覧を取得(チェックボックス用)
$permissions = Permission::query()
->orderBy('id')
->get(['id', 'code', 'name']);
// ※自治体IDopeに紐づく想定
$municipalityId = (int)($ope->municipality_id ?? 0);
if ($request->isMethod('get')) { if ($request->isMethod('get')) {
return view('admin.opes.edit', [ // edit.blade.php が参照する変数名に合わせて渡す
'isEdit' => true, return view('admin.opes.edit', array_merge(
'record' => $ope, [
'isEdit' => 1,
// ▼追加権限設定UI用 'isInfo' => 0,
'features' => $features, 'ope_id' => $ope->ope_id,
'permissions' => $permissions, ],
'selectedFeatureId' => old('feature_id', null), $ope->toArray()
]); ));
} }
/**
* ▼権限設定の保存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 = [ $rules = [
'login_id' => "required|string|max:255|unique:ope,login_id,{$id},ope_id",
'ope_name' => 'required|string|max:255', 'ope_name' => 'required|string|max:255',
'ope_type' => 'required|string|max:50', 'ope_type' => 'required|string|max:50',
'ope_mail' => 'nullable|email|max:255',
'ope_phone' => 'nullable|string|max:50', 'ope_phone' => 'nullable|string|max:50',
'ope_mail' => [
'required',
function ($attribute, $value, $fail) {
$emails = array_map('trim', explode(';', str_replace(',', ';', $value)));
foreach ($emails as $email) {
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$fail("無効なメールアドレス形式です: {$email}");
}
}
}
],
'password' => 'nullable|string|min:8|confirmed',
]; ];
$this->validate($request, $rules);
$request->validate($rules); $ope->fill($request->only($ope->getFillable()));
// --- 保存用にメールを ; 区切りに統一 ---
if (!empty($data['ope_mail'])) {
$emails = array_filter(array_map('trim', explode(';', str_replace(',', ';', $data['ope_mail']))));
$data['ope_mail'] = implode(';', $emails);
}
// パスワード空なら更新しない
if (empty($data['password'])) {
unset($data['password']);
}
// 保存処理
$ope->fill($data);
$ope->save(); $ope->save();
return redirect()->route('opes')->with('success', '更新しました。'); return redirect()->route('opes')->with('success', 'オペレータを更新しました。');
} }
/** /**
* 権限回顧AJAX * 詳細
* /opes/{id}/permissions?feature_id=xx
* ※ope_permissionが自治体単位のため、opeの自治体IDで取得する
*/ */
public function getPermissionsByFeature(int $id, Request $request) public function info($id)
{ {
$ope = Ope::getByPk($id); $ope = Ope::getByPk($id);
if (!$ope) abort(404); if (!$ope) abort(404);
$featureId = (int)$request->query('feature_id'); // info.blade.php が参照する変数に合わせてセット
if ($featureId <= 0) { return view('admin.opes.info', array_merge(
return response()->json([]); [
} 'isEdit' => 0,
'isInfo' => 1,
$municipalityId = (int)($ope->municipality_id ?? 0); 'ope_id' => $ope->ope_id,
],
$ids = OpePermission::query() $ope->toArray()
->where('municipality_id', $municipalityId) ));
->where('feature_id', $featureId)
->pluck('permission_id')
->values();
return response()->json($ids);
} }
/** /**
* 削除(単体 or 複数) * 削除(単体 / 複数)
*/ */
public function delete(Request $request) public function delete(Request $request)
{ {
$ids = []; $ids = [];
// 単体削除
if ($request->filled('id')) { if ($request->filled('id')) {
$ids[] = (int)$request->input('id'); $ids[] = (int) $request->input('id');
} }
if ($request->filled('ids') && is_array($request->input('ids'))) {
// 複数削除 $ids = array_merge($ids, array_map('intval', $request->input('ids')));
if ($request->filled('ids')) {
$ids = array_merge($ids, array_map('intval', (array)$request->input('ids')));
} }
$ids = array_values(array_unique($ids));
$ids = array_unique($ids);
if (!$ids) { if (!$ids) {
return back()->with('error', '削除対象が選択されていません。'); return back()->with('error', '削除対象が選択されていません。');

View File

@ -22,64 +22,39 @@ class OperatorQueController extends Controller
public function list(Request $request) public function list(Request $request)
{ {
$sort = $request->input('sort', 'que_id'); $sort = $request->input('sort', 'que_id');
$sort_type = $request->input('sort_type', 'asc'); $sort_type = $request->input('sort_type', 'desc');
$que_status = $request->input('que_status');
// 許可されたカラム名のリストDB定義に合わせて
$allowedSorts = ['que_id', 'ope_id', 'que_status', 'created_at', 'updated_at', 'user_id', 'park_id', 'que_class'];
if (!in_array($sort, $allowedSorts)) {
$sort = 'que_id';
}
if (!in_array($sort_type, ['asc', 'desc'])) {
$sort_type = 'desc';
}
$query = OperatorQue::query(); $query = OperatorQue::query();
// フィルタリング(絞り込み) if ($request->filled('que_status')) {
if (!empty($que_status)) { $query->where('que_status', $request->input('que_status'));
$query->where('que_status', $que_status);
} }
$list = $query->orderBy($sort, $sort_type) $list = $query->orderBy($sort, $sort_type)
->paginate(\App\Utils::item_per_page ?? 20); ->paginate(\App\Utils::item_per_page ?? 20);
// view に $que_status を渡す $que_status = $request->input('que_status');
return view('admin.operator_ques.list', compact('list', 'sort', 'sort_type', 'que_status'));
return view('admin.operator_ques.list', compact('list', 'sort', 'sort_type'));
} }
/** /**
* 新規登録(画面/処理) * 新規登録(画面/処理)
*/ */
public function add(Request $request) public function add(Request $request)
{ {
if ($request->isMethod('get')) { if ($request->isMethod('get')) {
// 新規時は空のレコードを用意してフォーム描画 // 新規時は空の値でフォーム描画
return view('admin.operator_ques.add', array_merge( return view('admin.operator_ques.add', $this->formPayload());
$this->formPayload(),
[
'isEdit' => false,
'record' => new OperatorQue(), // ← ★ _form.blade.php で使う用
'que_id' => null,
]
));
} }
// POST時バリデーション
$data = $this->validateRequest($request); $data = $this->validateRequest($request);
// 登録処理
OperatorQue::create($data); OperatorQue::create($data);
return redirect()->route('operator_ques')->with('success', '登録しました。'); return redirect()->route('operator_ques')->with('success', 'オペレーターキューを登録しました。');
} }
/** /**
* 編集(画面/処理) * 編集(画面/処理)
*/ */
@ -90,10 +65,7 @@ class OperatorQueController extends Controller
if ($request->isMethod('get')) { if ($request->isMethod('get')) {
return view('admin.operator_ques.edit', array_merge( return view('admin.operator_ques.edit', array_merge(
$this->formPayload($que), $this->formPayload($que),
[ ['que_id' => $que->que_id]
'que_id' => $que->que_id,
'record' => $que,
]
)); ));
} }
@ -101,7 +73,7 @@ class OperatorQueController extends Controller
$que->fill($data)->save(); $que->fill($data)->save();
return redirect()->route('operator_ques')->with('success', '更新しました。'); return redirect()->route('operator_ques')->with('success', 'オペレーターキューを更新しました。');
} }
/** /**
@ -250,9 +222,9 @@ class OperatorQueController extends Controller
private function validateRequest(Request $request, $queId = null): array private function validateRequest(Request $request, $queId = null): array
{ {
$rules = [ $rules = [
'user_id' => 'nullable|integer', 'user_id' => 'required|integer',
'contract_id' => 'nullable|integer', 'contract_id' => 'nullable|integer',
'park_id' => 'nullable|integer', 'park_id' => 'required|integer',
'que_class' => 'required|integer', 'que_class' => 'required|integer',
'que_comment' => 'nullable|string|max:2000', 'que_comment' => 'nullable|string|max:2000',
'que_status' => 'required|integer', 'que_status' => 'required|integer',

View File

@ -1,230 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use App\Models\Usertype;
use App\Models\Ope;
class PersonalController extends Controller
{
/**
* 本人確認手動処理 一覧画面
*/
public function list(Request $request)
{
$query = User::query();
if ($request->filled('user_id')) {
$query->where('user_id', $request->input('user_id'));
}
$users = $query->paginate(20);
return view('admin.personal.list', [
'users' => $users,
'request' => $request,
]);
}
/**
* 本人確認手動処理 編集画面
*/
public function edit(Request $request, $id)
{
// 利用者情報取得
$user = User::where('user_id', $id)->firstOrFail();
// 利用者分類マスタ取得(ラジオボタン用)
$usertypes = Usertype::orderBy('sort_order')->get();
// POST時の処理
if ($request->isMethod('post')) {
// 利用者分類IDの更新
$user->user_categoryid = $request->input('user_categoryid', $user->user_categoryid);
// 本人確認チェックOK/NG
if ($request->input('check') === 'ok') {
$user->user_idcard_chk_flag = 1;
} elseif ($request->input('check') === 'ng') {
$user->user_idcard_chk_flag = 0;
// 備考欄も更新NG理由
$user->user_remarks = $request->input('user_remarks', $user->user_remarks);
}
$user->save();
return redirect()->route('personal')->with('success', '更新しました');
}
return view('admin.personal.edit', [
'user' => $user,
'usertypes' => $usertypes,
]);
}
}
class OpesController extends Controller
{
/**
* オペレータ一覧画面
*/
public function list(Request $request)
{
$sort = $request->input('sort', 'ope_id'); // デフォルト値を設定
$sort_type = $request->input('sort_type', 'asc'); // デフォルト値を設定
$query = Ope::query();
// 並び替え
$query->orderBy($sort, $sort_type);
$list = $query->paginate(20);
return view('admin.opes.list', [
'list' => $list,
'sort' => $sort,
'sort_type' => $sort_type,
'request' => $request,
]);
}
/**
* オペレータ編集画面
*/
public function edit(Request $request, $id)
{
$ope = \App\Models\Ope::findOrFail($id);
if ($request->isMethod('post')) {
// バリデーション&更新処理
// ...
}
// 各項目を配列で渡す
return view('admin.opes.edit', [
'ope_id' => $ope->ope_id,
'ope_name' => $ope->ope_name,
'login_id' => $ope->login_id,
'ope_pass' => '', // パスワードは空で
'ope_belong' => $ope->ope_belong,
'ope_type' => $ope->ope_type,
'ope_mail' => $ope->ope_mail,
'ope_phone' => $ope->ope_phone,
'ope_sendalart_que1' => $ope->ope_sendalart_que1,
'ope_sendalart_que2' => $ope->ope_sendalart_que2,
'ope_sendalart_que3' => $ope->ope_sendalart_que3,
'ope_sendalart_que4' => $ope->ope_sendalart_que4,
'ope_sendalart_que5' => $ope->ope_sendalart_que5,
'ope_sendalart_que6' => $ope->ope_sendalart_que6,
'ope_sendalart_que7' => $ope->ope_sendalart_que7,
'ope_sendalart_que8' => $ope->ope_sendalart_que8,
'ope_sendalart_que9' => $ope->ope_sendalart_que9,
'ope_sendalart_que10' => $ope->ope_sendalart_que10,
'ope_sendalart_que11' => $ope->ope_sendalart_que11,
'ope_sendalart_que12' => $ope->ope_sendalart_que12,
'ope_sendalart_que13' => $ope->ope_sendalart_que13,
'ope_auth1' => $ope->ope_auth1,
'ope_auth2' => $ope->ope_auth2,
'ope_auth3' => $ope->ope_auth3,
'ope_auth4' => $ope->ope_auth4,
'ope_quit_flag' => $ope->ope_quit_flag,
'ope_quitday' => $ope->ope_quitday,
]);
}
/**
* オペレータ一覧のエクスポート
*/
public function export(Request $request)
{
$filename = 'ope_export_' . date('Ymd_His') . '.csv';
$columns = [
'ope_id', 'ope_belong', 'login_id', 'ope_name', 'ope_pass', 'ope_type', 'ope_mail', 'ope_phone',
'ope_sendalart_que1', 'ope_sendalart_que2', 'ope_sendalart_que3', 'ope_sendalart_que4', 'ope_sendalart_que5',
'ope_sendalart_que6', 'ope_sendalart_que7', 'ope_sendalart_que8', 'ope_sendalart_que9', 'ope_sendalart_que10',
'ope_sendalart_que11', 'ope_sendalart_que12', 'ope_sendalart_que13',
'ope_auth1', 'ope_auth2', 'ope_auth3', 'ope_auth4',
'ope_quit_flag', 'ope_quitday', 'created_at', 'updated_at'
];
$ids = $request->input('pk', []);
if (!empty($ids)) {
$list = \App\Models\Ope::whereIn('ope_id', $ids)->select($columns)->get();
} else {
$list = \App\Models\Ope::select($columns)->get();
}
$callback = function() use ($list, $columns) {
$file = fopen('php://output', 'w');
// ヘッダー
fputcsv($file, $columns);
foreach ($list as $row) {
$data = [];
foreach ($columns as $col) {
$data[] = $row->$col;
}
fputcsv($file, $data);
}
fclose($file);
};
return response()->stream($callback, 200, [
"Content-Type" => "text/csv",
"Content-Disposition" => "attachment; filename={$filename}",
]);
}
/**
* オペレータの削除
*/
public function delete(Request $request)
{
// チェックされたIDの配列を受け取る想定
$ids = $request->input('pk', []);
if (!empty($ids)) {
\App\Models\Ope::whereIn('ope_id', $ids)->delete();
return redirect()->route('opes')->with('success', '削除しました');
}
return redirect()->route('opes')->with('error', '削除対象が選択されていません');
}
/**
* オペレータの追加
*/
public function add(Request $request)
{
if ($request->isMethod('post')) {
$validated = $request->validate([
'ope_name' => 'required|string|max:255',
'login_id' => 'required|string|max:255|unique:ope,login_id',
'password' => 'required|string|min:6|confirmed',
'ope_type' => 'required',
'ope_mail' => 'required|email',
]);
$ope = new \App\Models\Ope();
$ope->ope_name = $request->ope_name;
$ope->login_id = $request->login_id;
$ope->ope_pass = bcrypt($request->password);
$ope->ope_type = $request->ope_type;
$ope->ope_mail = $request->ope_mail;
$ope->ope_phone = $request->ope_phone;
for ($i = 1; $i <= 13; $i++) {
$field = "ope_sendalart_que{$i}";
$ope->$field = $request->$field ?? 0;
}
for ($i = 1; $i <= 4; $i++) {
$field = "ope_auth{$i}";
$ope->$field = $request->$field ?? '';
}
$ope->ope_quit_flag = $request->ope_quit_flag ?? 0;
$ope->ope_quitday = $request->ope_quitday ?? null;
$ope->save();
return redirect()->route('opes')->with('success', '登録しました');
}
return view('admin.opes.add');
}
}

View File

@ -1,308 +1,266 @@
<?php <?php
declare(strict_types=1);
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\ParkRequest;
use App\Models\City; use App\Models\City;
use App\Services\ParkService; use App\Http\Requests\ParkRequest;
use App\Models\Park;
use App\Utils;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\StreamedResponse; use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Validator;
use Response;
class ParkController extends Controller class ParkController extends Controller
{ {
public function __construct( public function list(Request $request)
private readonly ParkService $parkService {
) { $query = \DB::table('park as p')
->leftJoin('city as c', 'p.city_id', '=', 'c.city_id')
->select([
'p.park_id',
'c.city_name',
'p.park_name',
'p.park_ruby',
'p.park_syllabary',
'p.park_adrs',
'p.park_close_flag',
'p.park_day',
'p.alert_flag',
'p.print_number',
'p.keep_alive',
]);
if ($request->filled('park_name')) {
$query->where('p.park_name', 'like', '%' . $request->input('park_name') . '%');
}
if ($request->filled('city_id')) {
$query->where('p.city_id', $request->input('city_id'));
}
if ($request->filled('sort')) {
$query->orderBy($request->input('sort'), $request->input('sort_type', 'asc'));
} else {
$query->orderBy('p.park_id', 'asc');
} }
/** $parks = $query->paginate(20);
* 一覧 $cities = \DB::table('city')->orderBy('city_id')->get();
*/
public function index(ParkRequest $request)
{
$filters = $request->filters();
$parks = $this->parkService->paginate($filters, 20); return view('admin.parks.list', compact('parks', 'cities'));
$cities = City::orderBy('city_id')->get();
$sort = $filters['sort'] ?? 'p.park_id';
$sort_type = $filters['sort_type'] ?? 'asc';
return view('admin.parks.index', compact(
'parks',
'cities',
'sort',
'sort_type'
));
} }
/** public function add(Request $request)
* 新規(画面)
*/
public function create()
{ {
$cities = City::orderBy('city_id')->get(); $cities = \DB::table('city')->orderBy('city_id')->get();
return view('admin.parks.create', compact('cities')); if ($request->isMethod('post')) {
// バリデーション(必要な項目だけ例示)
$validated = $request->validate([
'city_id' => 'required|integer',
'park_name' => 'required|string|max:255',
// 他の項目も必要に応じて追加
]);
// 保存処理
$park = new \App\Models\Park();
$park->fill($validated);
$park->save();
return redirect()->route('parks')->with('success', '登録しました');
} }
/** return view('admin.parks.add', [
* 新規(登録) 'cities' => $cities,
*/ ]);
public function store(ParkRequest $request) }
public function edit(Request $request, $pk, $view = '')
{ {
$operatorId = (int) (auth()->user()->ope_id ?? 1); $park = Park::getByPk($pk);
if (empty($pk) || empty($park)) {
abort('404');
}
$data = $park->getAttributes();
$dataList = $this->getDataDropList();
$data = array_merge($data, $dataList);
if ($request->isMethod('POST')) {
// ここをaddと同じバリデーションに変更
$validated = $request->validate([
'city_id' => 'required|integer',
'park_name' => 'required|string|max:255',
// 他の項目も必要に応じて追加
]);
$payload = $request->payload(); \DB::transaction(function () use ($validated, &$type, $park) {
$park->fill($validated);
$park->save();
$type = true;
});
$request->session()->flash('success', __('更新に成功しました'));
return redirect()->route('parks');
}
if ($view != '') {
return view($view, $data);
}
return view('admin.parks.edit', [
'park' => $park,
'cities' => $dataList['cities'] ?? [],
// 必要な他の変数もここで渡す
]);
}
// 駐輪場五十音を設定 public function delete(Request $request)
$payload['park_syllabary'] = $this->toSyllabaryGroup( {
$payload['park_ruby'] ?? null $arr_pk = $request->get('pk');
if ($arr_pk) {
if (Park::deleteByPk($arr_pk)) {
return redirect()->route('parks')->with('success', __("削除が完了しました。"));
} else {
return redirect()->route('parks')->with('error', __('削除に失敗しました。'));
}
}
return redirect()->route('parks')->with('error', __('削除するユーザーを選択してください。'));
}
public function info(Request $request, $id)
{
return $this->edit($request, $id, 'admin.parks.info');
}
public function getDataDropList()
{
$data['cities'] = City::orderBy('city_id')->get();
return $data;
}
public function export(Request $request)
{
$headers = array(
"Content-type" => "text/csv;charset=UTF-8",
'Content-Encoding: UTF-8',
"Content-Disposition" => "attachment; filename=file.csv",
"Pragma" => "no-cache",
"Cache-Control" => "must-revalidate, post-check=0, pre-check=0",
"Expires" => "0"
); );
$inputs = [
'isMethodPost' => 0,
'isExport' => 1,
'sort' => $request->input('sort', ''),
'sort_type' => $request->input('sort_type', ''),
// 駐輪場五十音を設定してから Service に渡す
$this->parkService->create(
$payload,
$operatorId
);
return redirect()
->route('parks.index')
->with('success', __('新規登録に完了しました。'));
}
/**
* 編集(画面)
*/
public function edit(int $id)
{
$record = $this->parkService->findOrFail($id);
$cities = City::orderBy('city_id')->get();
return view('admin.parks.edit', compact(
'record',
'cities'
));
}
/**
* 編集(更新)
*/
public function update(ParkRequest $request, int $id)
{
$park = $this->parkService->findOrFail($id);
$operatorId = (int) (auth()->user()->ope_id ?? 1);
$payload = $request->payload();
// 駐輪場五十音を設定
$payload['park_syllabary'] = $this->toSyllabaryGroup(
$payload['park_ruby'] ?? null);
// 駐輪場五十音を設定してから Service に渡す
$this->parkService->update(
$park, $payload, $operatorId);
$this->parkService->update(
$park,
$request->payload(),
$operatorId
);
return redirect()
->route('parks.index')
->with('success', __('更新に成功しました。'));
}
/**
* 削除(複数)
*/
public function destroy(Request $request)
{
$ids = (array) $request->input('pk', []);
if (empty($ids)) {
return redirect()
->route('parks.index')
->with('error', __('削除するデータを選択してください。'));
}
$ok = $this->parkService->deleteByIds($ids);
return redirect()
->route('parks.index')
->with(
$ok ? 'success' : 'error',
$ok ? __('削除が完了しました。') : __('削除に失敗しました。')
);
}
/**
* CSV 出力
*/
public function export(): StreamedResponse
{
$columns = [
'駐輪場ID','市区','駐輪場名','駐輪場ふりがな','駐輪場五十音','住所',
'閉設フラグ','閉設日','残警告チェックフラグ','印字数','最新キープアライブ',
'更新オペレータID','更新期間開始日','更新期間開始時',
'更新期間終了日','更新期間終了時','駐輪開始期間',
'リマインダー種別','リマインダー時間','契約後即利用許可',
'項目表示設定:性別','項目表示設定:生年月日','項目表示設定:防犯登録番号',
'二点間距離','駐車場座標(緯度)','駐車場座標(経度)','電話番号',
'駐輪場契約形態(定期)','駐輪場契約形態(一時利用)',
'車種制限','手続方法','支払方法',
'利用可能時間制限フラグ','利用可能時間(開始)','利用可能時間(終了)',
'常駐管理人フラグ','常駐時間(開始)','常駐時間(終了)',
'屋根フラグ','シール発行機フラグ','駐輪場利用方法',
'定期更新期間','空き待ち予約','特記事項','学生証確認種別',
'減免案内表示フラグ','減免対象年齢','減免案内表示開始月数','年跨ぎ',
]; ];
$rows = $this->parkService->exportRows(); $dataExport = Park::search($inputs);
$columns = array(
return response()->streamDownload(function () use ($rows, $columns) { __('駐輪場ID '),// 0
$fp = fopen('php://output', 'w'); __('市区ID'),// 1
__('市区'),// 2
fwrite($fp, "\xEF\xBB\xBF"); __('駐輪場名'),// 3
fputcsv($fp, $columns); __('駐輪場ふりがな'),// 4
__('駐輪場五十音'),// 5
foreach ($rows as $r) { __('住所'),// 6
fputcsv($fp, [ __('閉設フラグ'),// 7
$r->park_id, __('閉設フラグ'),// 8
$r->city_id, __('閉設日'),// 9
$r->park_name, __('残警告チェックフラグ'),// 10
$r->park_ruby, __('印字数'),// 11
$r->park_syllabary, __('最新キープアライブ')// 12
$r->park_adrs, );
$r->park_close_flag, $filename = "駐輪場マスタ.csv";
$r->park_day, $file = fopen($filename, 'w+');
$r->alert_flag, fputcsv($file, $columns);
$r->print_number, foreach ($dataExport as $items) {
$r->keep_alive, fputcsv(
$r->operator_id, $file,
$r->update_grace_period_start_date, array(
$r->update_grace_period_start_time, $items->park_id,// 0
$r->update_grace_period_end_date, $items->city_id,// 1
$r->update_grace_period_end_time, !empty($items->getCity()) ? $items->getCity()->city_name : "",// 2
$r->parking_start_grace_period, $items->park_name, // 3
$r->reminder_type, $items->park_ruby, // 4
$r->reminder_time, $items->park_syllabary, // 5
$r->immediate_use_permit, $items->park_adrs, // 6
$r->gender_display_flag, $items->park_close_flag,// 7
$r->bd_display_flag, $items->getParkCloseFlagDisplay(),// 8
$r->securityreg_display_flag, $items->park_day,// 9
$r->distance_twopoints, $items->alert_flag,// 10
$r->park_latitude, $items->print_number,// 11
$r->park_longitude, $items->keep_alive// 12
$r->park_tel, )
$r->park_fixed_contract, );
$r->park_temporary_contract, }
$r->park_restriction, fclose($file);
$r->park_procedure, return Response::download($filename, $filename, $headers);
$r->park_payment,
$r->park_available_time_flag,
$r->park_available_time_from,
$r->park_available_time_to,
$r->park_manager_flag,
$r->park_manager_resident_from,
$r->park_manager_resident_to,
$r->park_roof_flag,
$r->park_issuing_machine_flag,
$r->park_using_method,
$r->park_contract_renewal_term,
$r->park_reservation,
$r->park_reference,
$r->student_id_confirm_type,
$r->reduction_guide_display_flag,
$r->reduction_age,
$r->reduction_guide_display_start_month,
$r->overyear_flag,
]);
} }
fclose($fp); public function import(Request $request)
}, '駐輪場マスタ.csv', [
'Content-Type' => 'text/csv; charset=UTF-8',
]);
}
/**
* 重複チェックAJAX
*/
public function checkDuplicate(Request $request)
{ {
$parkName = (string) $request->input('park_name', ''); $file = $request->file('file');
if (!empty($file)) {
if ($parkName === '') { $data = Utils::csvToArray($file);
return response()->json(['duplicate' => false]); $type = 1;
} $msg = '';
$record = 0;
$dup = $this->parkService->checkDuplicateByName($parkName); DB::beginTransaction();
try {
if (!$dup) { Park::query()->delete();
return response()->json(['duplicate' => false]); $col = 13;
foreach ($data as $key => $items) {
$record = $key + 2;
if (count($items) == $col) {
$row = new Park();
$row->park_id = $items[0];
$row->city_id = $items[1];
$row->park_name = $items[3];
$row->park_ruby = $items[4];
$row->park_syllabary = $items[5];
$row->park_adrs = $items[6];
$row->park_close_flag = $items[7];
$row->park_day = $items[9];
$row->alert_flag = $items[10];
$row->print_number = $items[11];
$row->keep_alive = $items[12];
if (!$row->save()) {
$type = 0;
$msg = '行:record型が一致しません。';
break;
}
} else {
$type = 0;
$msg = '行:record列数が一致しません。';
break;
}
}
} catch (\Exception $e) {
$msg = '行:record型が一致しません。';
$type = 0;
}
if ($type) {
DB::commit();
return redirect()->route('parks')->with('success', __('輸入成功'));
} else {
DB::rollBack();
return redirect()->route('parks')->with('error', __($msg, ['record' => $record]));
}
} else {
return redirect()->route('parks')->with('error', __('あなたはcsvファイルを選択していません。'));
}
} }
public function checkDuplicate(\Illuminate\Http\Request $request)
{
$parkName = $request->input('park_name');
$duplicate = Park::where('park_name', $parkName)->first();
if ($duplicate) {
return response()->json([ return response()->json([
'duplicate' => true, 'duplicate' => true,
'park_id' => $dup->park_id, 'park_id' => $duplicate->park_id,
'park_name' => $dup->park_name, 'park_name' => $duplicate->park_name,
]); ]);
} }
return response()->json(['duplicate' => false]);
private function toSyllabaryGroup(?string $ruby): ?string
{
$ruby = trim((string) $ruby);
if ($ruby === '') {
return null;
} }
// 先取第1文字UTF-8
$first = mb_substr($ruby, 0, 1, 'UTF-8');
// 小书き/濁点などを正規化(必要最低限)
$map = [
'が'=>'か','ぎ'=>'き','ぐ'=>'く','げ'=>'け','ご'=>'こ',
'ざ'=>'さ','じ'=>'し','ず'=>'す','ぜ'=>'せ','ぞ'=>'そ',
'だ'=>'た','ぢ'=>'ち','づ'=>'つ','で'=>'て','ど'=>'と',
'ば'=>'は','び'=>'ひ','ぶ'=>'ふ','べ'=>'へ','ぼ'=>'ほ',
'ぱ'=>'は','ぴ'=>'ひ','ぷ'=>'ふ','ぺ'=>'へ','ぽ'=>'ほ',
'ぁ'=>'あ','ぃ'=>'い','ぅ'=>'う','ぇ'=>'え','ぉ'=>'お',
'ゃ'=>'や','ゅ'=>'ゆ','ょ'=>'よ','っ'=>'つ',
'ゎ'=>'わ',
];
$first = $map[$first] ?? $first;
// グループ判定(先頭文字で分類)
$groups = [
'あ' => ['あ','い','う','え','お'],
'か' => ['か','き','く','け','こ'],
'さ' => ['さ','し','す','せ','そ'],
'た' => ['た','ち','つ','て','と'],
'な' => ['な','に','ぬ','ね','の'],
'は' => ['は','ひ','ふ','へ','ほ'],
'ま' => ['ま','み','む','め','も'],
'や' => ['や','ゆ','よ'],
'ら' => ['ら','り','る','れ','ろ'],
'わ' => ['わ','を','ん'],
];
foreach ($groups as $head => $chars) {
if (in_array($first, $chars, true)) {
return $head;
}
}
// ひらがな以外(空/英数など)は null
return null;
}
} }

View File

@ -1,204 +0,0 @@
<?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', '削除しました。');
}
}

View File

@ -44,7 +44,6 @@ class PaymentController extends Controller
'payment_inquiryname' => 'nullable|string|max:255', 'payment_inquiryname' => 'nullable|string|max:255',
'payment_inquirytel' => 'nullable|string|max:255', 'payment_inquirytel' => 'nullable|string|max:255',
'payment_time' => 'nullable|string|max:255', 'payment_time' => 'nullable|string|max:255',
]); ]);
// 登録データ作成 // 登録データ作成
@ -53,7 +52,7 @@ class PaymentController extends Controller
Payment::create($data); Payment::create($data);
return redirect()->route('payments')->with('success', '登録しました'); return redirect()->route('payments')->with('success', '登録しました');
} }
return view('admin.payments.add', [ return view('admin.payments.add', [
@ -81,7 +80,7 @@ class PaymentController extends Controller
$payment->update($data); $payment->update($data);
return redirect()->route('payments')->with('success', '更新しました'); return redirect()->route('payments')->with('success', '更新しました');
} }
return view('admin.payments.edit', [ return view('admin.payments.edit', [
@ -109,22 +108,11 @@ class PaymentController extends Controller
*/ */
public function delete(Request $request) public function delete(Request $request)
{ {
$pk = $request->input('pk', []); if ($request->has('ids')) {
Payment::whereIn('payment_id', $request->ids)->delete();
// 配列に統一 return redirect()->route('payments')->with('success', '削除しました');
$ids = is_array($pk) ? $pk : [$pk];
// 数字チェック
$ids = array_values(array_filter($ids, fn($v) => preg_match('/^\d+$/', (string) $v)));
if (empty($ids)) {
return redirect()->route('payments')->with('error', '削除対象が選択されていません。');
} }
return redirect()->route('payments')->with('error', '削除対象が選択されていません');
// 削除
Payment::whereIn('payment_id', $ids)->delete();
return redirect()->route('payments')->with('success', '削除しました。');
} }
/** /**

View File

@ -10,13 +10,13 @@ class PeriodicalController extends Controller
// 画面 // 画面
public function list(Request $request) public function list(Request $request)
{ {
$parks = DB::table('regular_contract') // Bladeで使うカラム名id, nameで取得
->join('park', 'regular_contract.park_id', '=', 'park.park_id') $parks = DB::table('park')
->select('park.park_id', 'park.park_name') ->select('park_id', 'park_name')
->distinct() ->orderBy('park_name')
->orderBy('park.park_name')
->get(); ->get();
// 必要なら選択中のpark_idも渡す
$selectedParkId = $request->input('park_id', ''); $selectedParkId = $request->input('park_id', '');
return view('admin.periodical.list', compact('parks', 'selectedParkId')); return view('admin.periodical.list', compact('parks', 'selectedParkId'));
@ -27,365 +27,73 @@ class PeriodicalController extends Controller
{ {
$parkId = $request->input('park_id'); $parkId = $request->input('park_id');
if (empty($parkId)) { // 契約状況
return response()->json([ $bicycleGeneral = DB::table('regular_contract')
'contract_summary' => [], ->where('park_id', $parkId)
'waiting_summary' => [], ->where('user_categoryid', 1)
'renewal_summary' => [], ->where('ptype_id', 1) // 1:自転車
'debug_info' => ['message' => 'No park_id provided'] ->where('contract_cancel_flag', 0)
]); ->count();
} $bicycleStudent = DB::table('regular_contract')
->where('park_id', $parkId)
->where('user_categoryid', 2)
->where('ptype_id', 1)
->where('contract_cancel_flag', 0)
->count();
// 原付・その他も同様に集計
// デバッグ情報を収集 $contractSummary = [
$debugInfo = [ [
'park_id' => $parkId, 'type' => '自転車',
'has_regular_type_id' => false, 'general_count' => $bicycleGeneral,
'contract_count' => 0,
'waiting_count' => 0,
'renewal_count' => 0
];
// 契約状況を車種別に集計
$contractData = [
['psection_id' => 1, 'type' => '自転車'],
['psection_id' => 2, 'type' => '原付'],
['psection_id' => 0, 'type' => 'その他'] // その他は psection_id が1,2以外
];
$contractSummary = [];
$totalGeneral = 0;
$totalStudent = 0;
$totalUseTotal = 0;
$totalVacancy = 0;
$totalAll = 0;
foreach ($contractData as $vehicleType) {
$query = DB::table('regular_contract as rc')
->leftJoin('psection as ps', 'rc.psection_id', '=', 'ps.psection_id')
->selectRaw('SUM(CASE WHEN rc.user_categoryid = 1 THEN 1 ELSE 0 END) AS general_count')
->selectRaw('SUM(CASE WHEN rc.user_categoryid = 2 THEN 1 ELSE 0 END) AS student_count')
->selectRaw('SUM(CASE WHEN rc.contract_cancel_flag = 0 THEN 1 ELSE 0 END) AS use_total')
->selectRaw('SUM(CASE WHEN rc.contract_cancel_flag = 1 THEN 1 ELSE 0 END) AS vacancy')
->selectRaw('COUNT(*) AS total')
->where('rc.park_id', $parkId);
if ($vehicleType['psection_id'] === 0) {
// その他psection_id が 1,2 以外
$query->whereNotIn('rc.psection_id', [1, 2]);
} else {
// 自転車または原付
$query->where('rc.psection_id', $vehicleType['psection_id']);
}
$result = $query->first();
$generalCount = (int) ($result->general_count ?? 0);
$studentCount = (int) ($result->student_count ?? 0);
$useTotal = (int) ($result->use_total ?? 0);
$vacancy = (int) ($result->vacancy ?? 0);
$total = (int) ($result->total ?? 0);
// 最古の予約日と契約日を取得
$minReserveQuery = DB::table('regular_contract as rc2')
->join('reserve as r2', 'rc2.contract_id', '=', 'r2.contract_id')
->where('rc2.park_id', $parkId)
->where('r2.valid_flag', 1)
->where(function ($q) {
$q->whereNull('r2.reserve_cancel_flag')->orWhere('r2.reserve_cancel_flag', 0);
});
if ($vehicleType['psection_id'] === 0) {
$minReserveQuery->whereNotIn('rc2.psection_id', [1, 2]);
} else {
$minReserveQuery->where('rc2.psection_id', $vehicleType['psection_id']);
}
$minReserveDate = null;
$minContractDate = null;
$reserveResult = $minReserveQuery->orderBy('r2.reserve_date', 'asc')->first(['r2.reserve_date']);
if ($reserveResult) {
$minReserveDate = date('Y/m/d', strtotime($reserveResult->reserve_date));
}
$contractResult = $minReserveQuery->orderBy('r2.reserve_end', 'asc')->first(['r2.reserve_end']);
if ($contractResult) {
$minContractDate = date('Y/m/d', strtotime($contractResult->reserve_end));
}
$contractSummary[] = [
'type' => $vehicleType['type'],
'general_count' => $generalCount,
'general_extra' => '', 'general_extra' => '',
'student_count' => $studentCount, 'student_count' => $bicycleStudent,
'student_extra' => '', 'student_extra' => '',
'use_total' => $useTotal, 'use_total' => $bicycleGeneral + $bicycleStudent,
'vacancy' => $vacancy, 'vacancy' => 8, // 例: 空き数
'total' => $total, 'total' => $bicycleGeneral + $bicycleStudent + 8,
'last' => ['reserve_date' => $minReserveDate, 'contract_date' => $minContractDate], 'last' => [
'reserve_date' => '2025/07/27',
'contract_date' => '',
],
],
// 原付・その他も同様に
]; ];
$totalGeneral += $generalCount; // 空き待ち状況
$totalStudent += $studentCount; $waitingSummary = [
$totalUseTotal += $useTotal; [
$totalVacancy += $vacancy; 'type' => '自転車',
$totalAll += $total; 'general_count' => 28,
} 'general_head' => '2023/03/08',
'student_count' => 6,
// 合計行を追加 'student_head' => '2023/11/04',
$contractSummary[] = [ 'total' => 34,
'type' => '計', ],
'general_count' => $totalGeneral, // 原付・その他も同様に
'general_extra' => '',
'student_count' => $totalStudent,
'student_extra' => '',
'use_total' => $totalUseTotal,
'vacancy' => $totalVacancy,
'total' => $totalAll,
'last' => ['reserve_date' => null, 'contract_date' => null],
]; ];
$debugInfo['contract_count'] = count($contractSummary); // 更新状況
$renewalSummary = [];
// 空き待ち状況を車種別に集計 for ($m = 1; $m <= 12; $m++) {
$waitingData = [ $renewalSummary[] = [
['psection_id' => 1, 'type' => '自転車'], 'month' => sprintf('%02d月', $m),
['psection_id' => 2, 'type' => '原付'],
['psection_id' => 0, 'type' => 'その他'] // その他は psection_id が1,2以外
];
$waitingSummary = [];
$totalGeneral = 0;
$totalStudent = 0;
$totalAll = 0;
foreach ($waitingData as $vehicleType) {
$query = DB::table('regular_contract as rc')
->join('reserve as r', 'rc.contract_id', '=', 'r.contract_id')
->selectRaw('SUM(CASE WHEN rc.user_categoryid = 1 THEN 1 ELSE 0 END) AS general_count')
->selectRaw('DATE_FORMAT(MIN(CASE WHEN rc.user_categoryid = 1 THEN r.reserve_date END), "%Y/%m/%d") AS general_first')
->selectRaw('SUM(CASE WHEN rc.user_categoryid = 2 THEN 1 ELSE 0 END) AS student_count')
->selectRaw('DATE_FORMAT(MIN(CASE WHEN rc.user_categoryid = 2 THEN r.reserve_date END), "%Y/%m/%d") AS student_first')
->selectRaw('COUNT(*) AS total')
->where('rc.park_id', $parkId)
->where('r.valid_flag', 1)
->where(function ($q) {
$q->whereNull('r.reserve_cancel_flag')->orWhere('r.reserve_cancel_flag', 0);
});
if ($vehicleType['psection_id'] === 0) {
// その他psection_id が 1,2 以外
$query->whereNotIn('rc.psection_id', [1, 2]);
} else {
// 自転車または原付
$query->where('rc.psection_id', $vehicleType['psection_id']);
}
$result = $query->first();
$generalCount = (int) ($result->general_count ?? 0);
$studentCount = (int) ($result->student_count ?? 0);
$typeTotal = $generalCount + $studentCount;
$waitingSummary[] = [
'type' => $vehicleType['type'],
'general_count' => $generalCount,
'general_first' => $result->general_first ?? null,
'student_count' => $studentCount,
'student_first' => $result->student_first ?? null,
'total' => $typeTotal,
];
$totalGeneral += $generalCount;
$totalStudent += $studentCount;
$totalAll += $typeTotal;
}
// 合計行を追加
$waitingSummary[] = [
'type' => '計',
'general_count' => $totalGeneral,
'general_first' => null,
'student_count' => $totalStudent,
'student_first' => null,
'total' => $totalAll,
];
$debugInfo['waiting_count'] = count($waitingSummary);
// まず期間タイプフィールドが存在するかチェック
$hasRegularTypeId = false;
try {
$columns = DB::select('DESCRIBE regular_contract');
foreach ($columns as $column) {
if ($column->Field === 'regular_type_id') {
$hasRegularTypeId = true;
break;
}
}
} catch (\Exception $e) {
// エラーの場合はデフォルトで false
}
$debugInfo['has_regular_type_id'] = $hasRegularTypeId;
$renewalQuery = DB::table('regular_contract as rc')
->join('reserve as r', 'rc.contract_id', '=', 'r.contract_id')
->where('rc.park_id', $parkId)
->whereNotNull('r.reserve_start')
->where('r.valid_flag', 1)
->selectRaw("DATE_FORMAT(r.reserve_start, '%Y-%m') AS month");
if ($hasRegularTypeId) {
$renewalQuery->selectRaw('COALESCE(rc.regular_type_id, 1) AS period_type');
} else {
$renewalQuery->selectRaw('1 AS period_type'); // デフォルト値
}
$renewalSummary = $renewalQuery
->selectRaw('SUM(CASE WHEN rc.psection_id = 1 AND rc.user_categoryid = 1 THEN 1 ELSE 0 END) AS bicycle_general')
->selectRaw('SUM(CASE WHEN rc.psection_id = 1 AND rc.user_categoryid = 2 THEN 1 ELSE 0 END) AS bicycle_student')
->selectRaw('SUM(CASE WHEN rc.psection_id = 2 AND rc.user_categoryid = 1 THEN 1 ELSE 0 END) AS moped_general')
->selectRaw('SUM(CASE WHEN rc.psection_id = 2 AND rc.user_categoryid = 2 THEN 1 ELSE 0 END) AS moped_student')
->selectRaw('SUM(CASE WHEN rc.psection_id NOT IN (1,2) AND rc.user_categoryid = 1 THEN 1 ELSE 0 END) AS other_general')
->selectRaw('SUM(CASE WHEN rc.psection_id NOT IN (1,2) AND rc.user_categoryid = 2 THEN 1 ELSE 0 END) AS other_student')
->groupBy(['month', 'period_type'])
->orderBy('month')
->orderBy('period_type')
->limit(50)
->get();
// 月別期間別データを構造化
$structuredData = [];
$periodLabels = [
1 => '1ヶ月',
2 => '2ヶ月',
3 => '3ヶ月',
6 => '6ヶ月',
12 => '12ヶ月'
];
// regular_type_id がない場合のデフォルト処理
if (!$hasRegularTypeId) {
// フィールドがない場合は、月ごとに「計」行のみ表示
$monthlyTotals = [];
foreach ($renewalSummary as $row) {
$monthKey = $row->month;
if (!isset($monthlyTotals[$monthKey])) {
$monthlyTotals[$monthKey] = [
'bicycle_general' => 0, 'bicycle_general' => 0,
'bicycle_student' => 0, 'bicycle_student' => 0,
'bicycle_total' => 0,
'moped_general' => 0, 'moped_general' => 0,
'moped_student' => 0, 'moped_student' => 0,
'other_general' => 0, 'moped_total' => 0,
'other_student' => 0, 'others_general' => 0,
'others_student' => 0,
'others_total' => 0,
]; ];
} }
$monthlyTotals[$monthKey]['bicycle_general'] += (int) $row->bicycle_general;
$monthlyTotals[$monthKey]['bicycle_student'] += (int) $row->bicycle_student;
$monthlyTotals[$monthKey]['moped_general'] += (int) $row->moped_general;
$monthlyTotals[$monthKey]['moped_student'] += (int) $row->moped_student;
$monthlyTotals[$monthKey]['other_general'] += (int) $row->other_general;
$monthlyTotals[$monthKey]['other_student'] += (int) $row->other_student;
}
foreach ($monthlyTotals as $monthKey => $totals) {
$structuredData[$monthKey] = [
'month' => $monthKey,
'rows' => [[
'label' => '計',
'bicycle_general' => $totals['bicycle_general'],
'bicycle_student' => $totals['bicycle_student'],
'bicycle_total' => $totals['bicycle_general'] + $totals['bicycle_student'],
'moped_general' => $totals['moped_general'],
'moped_student' => $totals['moped_student'],
'moped_total' => $totals['moped_general'] + $totals['moped_student'],
'other_general' => $totals['other_general'],
'other_student' => $totals['other_student'],
'other_total' => $totals['other_general'] + $totals['other_student'],
]]
];
}
} else {
// regular_type_id がある場合の通常処理
foreach ($renewalSummary as $row) {
$monthKey = $row->month;
$periodType = (int)$row->period_type;
$periodLabel = $periodLabels[$periodType] ?? "{$periodType}ヶ月";
if (!isset($structuredData[$monthKey])) {
$structuredData[$monthKey] = [
'month' => $monthKey,
'rows' => [],
'totals' => [
'bicycle_general' => 0,
'bicycle_student' => 0,
'moped_general' => 0,
'moped_student' => 0,
'other_general' => 0,
'other_student' => 0,
]
];
}
$bicycle_total = (int) $row->bicycle_general + (int) $row->bicycle_student;
$moped_total = (int) $row->moped_general + (int) $row->moped_student;
$other_total = (int) $row->other_general + (int) $row->other_student;
// 期間別データを追加
$structuredData[$monthKey]['rows'][] = [
'label' => $periodLabel,
'bicycle_general' => (int) $row->bicycle_general,
'bicycle_student' => (int) $row->bicycle_student,
'bicycle_total' => $bicycle_total,
'moped_general' => (int) $row->moped_general,
'moped_student' => (int) $row->moped_student,
'moped_total' => $moped_total,
'other_general' => (int) $row->other_general,
'other_student' => (int) $row->other_student,
'other_total' => $other_total,
];
// 月別合計を計算
$structuredData[$monthKey]['totals']['bicycle_general'] += (int) $row->bicycle_general;
$structuredData[$monthKey]['totals']['bicycle_student'] += (int) $row->bicycle_student;
$structuredData[$monthKey]['totals']['moped_general'] += (int) $row->moped_general;
$structuredData[$monthKey]['totals']['moped_student'] += (int) $row->moped_student;
$structuredData[$monthKey]['totals']['other_general'] += (int) $row->other_general;
$structuredData[$monthKey]['totals']['other_student'] += (int) $row->other_student;
}
// 各月に計行を追加
foreach ($structuredData as &$monthData) {
$totals = $monthData['totals'];
$monthData['rows'][] = [
'label' => '計',
'bicycle_general' => $totals['bicycle_general'],
'bicycle_student' => $totals['bicycle_student'],
'bicycle_total' => $totals['bicycle_general'] + $totals['bicycle_student'],
'moped_general' => $totals['moped_general'],
'moped_student' => $totals['moped_student'],
'moped_total' => $totals['moped_general'] + $totals['moped_student'],
'other_general' => $totals['other_general'],
'other_student' => $totals['other_student'],
'other_total' => $totals['other_general'] + $totals['other_student'],
];
}
}
$renewalSummary = array_values($structuredData);
$debugInfo['renewal_count'] = count($renewalSummary);
return response()->json([ return response()->json([
'contract_summary' => $contractSummary, 'contract_summary' => $contractSummary,
'waiting_summary' => $waitingSummary, 'waiting_summary' => $waitingSummary,
'renewal_summary' => $renewalSummary, 'renewal_summary' => $renewalSummary,
'debug_info' => $debugInfo
]); ]);
} }
} }
?>

View File

@ -1,167 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use App\Models\Usertype;
class PersonalController extends Controller
{
/**
* 本人確認手動処理 一覧画面
*/
public function list(Request $request)
{
$query = User::query()
// 本人確認手動処理:未チェック(1) または 手動NG(4) または 自動チェックNG(5)
->whereIn('user_idcard_chk_flag', [1, 4, 5])
// 本人確認書類アップロード済み
->where(function($q) {
$q->whereNotNull('photo_filename1')
->orWhereNotNull('photo_filename2');
})
// usertypeテーブルとLEFT JOINで分類情報を取得
->leftJoin('usertype', 'user.user_categoryid', '=', 'usertype.user_categoryid')
->select('user.*',
'usertype.usertype_subject1',
'usertype.usertype_subject2',
'usertype.usertype_subject3');
if ($request->filled('user_id')) {
$query->where('user.user_id', $request->input('user_id'));
}
// データベースの物理順序(主キー昇順)で表示
$users = $query->paginate(20);
return view('admin.personal.list', [
'users' => $users,
'request' => $request,
]);
}
/**
* 本人確認手動処理 編集画面
*/
public function edit(Request $request, $seq)
{
\Log::info('=== Personal Edit Method START ===', ['seq' => $seq, 'method' => $request->method()]);
// 利用者情報取得user_seqで検索
$user = User::where('user_seq', $seq)->firstOrFail();
\Log::info('User found:', [
'user_seq' => $user->user_seq,
'user_id' => $user->user_id,
'current_flag' => $user->user_idcard_chk_flag
]);
// 利用者分類マスタ取得(ラジオボタン用)
$usertypes = Usertype::orderBy('sort_order')->get();
// POST時の処理
if ($request->isMethod('post')) {
\Log::info('=== FULL REQUEST DEBUG ===');
\Log::info('All request data:', $request->all());
\Log::info('=== Personal Edit POST Processing ===');
// 各フィールドの更新
$user->user_categoryid = $request->input('user_categoryid', $user->user_categoryid);
$user->user_regident_zip = $request->input('user_regident_zip', $user->user_regident_zip);
$user->user_regident_pre = $request->input('user_regident_pre', $user->user_regident_pre);
$user->user_regident_city = $request->input('user_regident_city', $user->user_regident_city);
$user->user_regident_add = $request->input('user_regident_add', $user->user_regident_add);
$user->user_relate_zip = $request->input('user_relate_zip', $user->user_relate_zip);
$user->user_relate_pre = $request->input('user_relate_pre', $user->user_relate_pre);
$user->user_relate_city = $request->input('user_relate_city', $user->user_relate_city);
$user->user_relate_add = $request->input('user_relate_add', $user->user_relate_add);
$user->user_remarks = $request->input('user_remarks', $user->user_remarks);
$user->user_idcard = $request->input('user_idcard', $user->user_idcard);
// 本人確認チェック処理(バックアップ値を優先使用)
$checkValue = $request->input('check') ?? $request->input('check_backup');
\Log::info('Check value received:', [
'check' => $request->input('check'),
'check_backup' => $request->input('check_backup'),
'final_value' => $checkValue,
'type' => gettype($checkValue)
]);
if ($checkValue === 'ok') {
$user->user_idcard_chk_flag = 3; // 手動チェックOK
$user->ope_id = auth()->user()->ope_id ?? auth()->id(); // 現在ログイン中の操作者ID
\Log::info('Setting user_idcard_chk_flag to 3 (手動チェックOK)');
\Log::info('Setting ope_id to current user:', ['ope_id' => $user->ope_id]);
} elseif ($checkValue === 'ng') {
$user->user_idcard_chk_flag = 4; // 手動チェックNG
$user->ope_id = auth()->user()->ope_id ?? auth()->id(); // 現在ログイン中の操作者ID
\Log::info('Setting user_idcard_chk_flag to 4 (手動チェックNG)');
\Log::info('Setting ope_id to current user:', ['ope_id' => $user->ope_id]);
} else {
\Log::warning('No valid check value received', [
'checkValue' => $checkValue,
'all_input' => $request->all()
]);
}
\Log::info('Before save:', [
'user_idcard_chk_flag' => $user->user_idcard_chk_flag,
'ope_id' => $user->ope_id,
'isDirty' => $user->isDirty(),
'getDirty' => $user->getDirty()
]);
try {
$result = $user->save();
\Log::info('User saved successfully', [
'result' => $result,
'user_idcard_chk_flag' => $user->user_idcard_chk_flag
]);
// 保存結果の検証
$user->refresh(); // モデルデータをリフレッシュ
$savedUser = User::where('user_seq', $seq)->first();
\Log::info('Verification after save', [
'model_refresh' => $user->user_idcard_chk_flag,
'user_idcard_chk_flag' => $savedUser->user_idcard_chk_flag
]);
// データベース直接確認
$dbUser = \DB::table('user')->where('user_seq', $seq)->first();
\Log::info('DB direct check:', [
'user_idcard_chk_flag' => $dbUser->user_idcard_chk_flag ?? 'NOT_FOUND'
]);
} catch (\Exception $e) {
\Log::error('Save failed', [
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
return back()->withErrors('保存に失敗しました: ' . $e->getMessage());
}
\Log::info('=== POST Processing END ===');
// 成功メッセージ
$message = 'データを更新しました。';
if ($checkValue === 'ok') {
$message = '本人確認チェックOKで更新しました。';
} elseif ($checkValue === 'ng') {
$message = '本人確認チェックNGで更新しました。';
}
return redirect()->route('personal')->with('success', $message);
}
\Log::info('=== Personal Edit Method END (GET) ===');
return view('admin.personal.edit', [
'user' => $user,
'usertypes' => $usertypes,
]);
}
}

View File

@ -24,152 +24,107 @@ class PplaceController extends Controller
$inputs['list'] = Pplace::search($inputs); $inputs['list'] = Pplace::search($inputs);
if ($inputs['list']->total() > 0 && $inputs['page'] > $inputs['list']->lastPage()) { if ($inputs['list']->total() > 0 && $inputs['page'] > $inputs['list']->lastPage()) {
return redirect()->route('pplaces'); return redirect()->route('pplace');
} }
return view('admin.pplace.list', $inputs); return view('admin.Pplace.list', $inputs);
} }
/**
* 新規登録(画面/処理)
*/
public function add(Request $request) public function add(Request $request)
{ {
if ($request->isMethod('get')) { $inputs = [
// 新規時:空のレコードとオペレーターリストを渡す 'pplace_number' => $request->input('pplace_number'),
return view('admin.pplace.add', [ 'pplace_remarks' => $request->input('pplace_remarks'),
'isEdit' => false, 'operator_id' => $request->input('operator_id'),
'record' => new Pplace(), ];
'operators' => Ope::getList(),
]);
}
// POST時バリデーション $inputs['operators'] = Ope::getList(); //
$rules = [
if ($request->isMethod('POST')) {
$validator = Validator::make($inputs, [
'pplace_number' => 'required|string|max:255', 'pplace_number' => 'required|string|max:255',
'pplace_remarks' => 'nullable|string|max:255', 'pplace_remarks' => 'nullable|string|max:255',
'operator_id' => 'nullable|integer', 'operator_id' => 'nullable|integer',
]; ]);
$messages = [
'pplace_number.required' => '駐輪場所番号は必須です。',
];
$validator = Validator::make($request->all(), $rules, $messages); if (!$validator->fails()) {
DB::transaction(function () use ($inputs) {
if ($validator->fails()) { $pplace = new Pplace();
return redirect()->back() $pplace->fill($inputs);
->withErrors($validator) $pplace->save();
->withInput()
->with(['operators' => Ope::getList()]);
}
// トランザクションで登録処理
DB::transaction(function () use ($request) {
$new = new Pplace();
$new->fill($request->only(['pplace_number', 'pplace_remarks', 'operator_id']));
$new->save();
}); });
return redirect()->route('pplace')->with('success', '登録成功');
return redirect()->route('pplaces')->with('success', '登録しました。'); } else {
$inputs['errorMsg'] = $this->__buildErrorMessasges($validator);
}
} }
return view('admin.Pplace.add', $inputs);
}
/** public function edit(Request $request, $id, $view = '')
* 編集(画面/処理)
*/
public function edit(Request $request, $id)
{ {
// 該当データ取得
$record = Pplace::find($id); $record = Pplace::find($id);
if (!$record) {
abort(404);
}
// オペレーターリスト取得(常に渡す) if (!$record) abort(404);
$operators = Ope::getList();
if ($request->isMethod('get')) { $data = $record->toArray();
// 編集画面表示 $data['operators'] = Ope::getList();
return view('admin.pplace.edit', [
'isEdit' => true,
'record' => $record,
'operators' => $operators,
]);
}
// POST時バリデーション
$rules = [ if ($request->isMethod('POST')) {
$inputs = $request->all();
$validator = Validator::make($inputs, [
'pplace_number' => 'required|string|max:255', 'pplace_number' => 'required|string|max:255',
'pplace_remarks' => 'nullable|string|max:255', 'pplace_remarks' => 'nullable|string|max:255',
'operator_id' => 'nullable|integer', 'operator_id' => 'nullable|integer',
]; ]);
$messages = [
'pplace_number.required' => '駐輪場所番号は必須です。',
];
$validator = Validator::make($request->all(), $rules, $messages); $data = array_merge($data, $inputs);
if ($validator->fails()) { if (!$validator->fails()) {
return redirect()->back() DB::transaction(function () use ($record, $inputs) {
->withErrors($validator) $record->fill($inputs);
->withInput()
->with(['operators' => $operators]);
}
// 更新処理
DB::transaction(function () use ($request, $record) {
$record->fill($request->only(['pplace_number', 'pplace_remarks', 'operator_id']));
$record->save(); $record->save();
}); });
return redirect()->route('pplace')->with('success', '更新成功');
return redirect()->route('pplaces')->with('success', '更新しました。'); } else {
$data['errorMsg'] = $this->__buildErrorMessasges($validator);
}
} }
return view($view ?: 'admin.Pplace.edit', $data);
}
public function info(Request $request, $id)
{
return $this->edit($request, $id, 'admin.Pplace.info');
}
/**
* 削除(単一/複数対応)
*/
public function delete(Request $request) public function delete(Request $request)
{ {
$ids = []; $pk = $request->get('pk');
if ($pk && Pplace::destroy($pk)) {
// 単一削除id return redirect()->route('pplace')->with('success', '削除成功');
if ($request->filled('id')) {
$ids[] = (int) $request->input('id');
} }
return redirect()->route('pplace')->with('error', '削除失敗');
// 複数削除(チェックボックス pk[]
if (is_array($request->input('pk'))) {
$ids = array_merge($ids, $request->input('pk'));
} }
// 重複除去 & 数値変換
$ids = array_values(array_unique(array_map('intval', $ids)));
// 対象未選択
if (empty($ids)) {
return back()->with('error', '削除対象が選択されていません。');
}
// 削除実行
Pplace::whereIn('pplace_id', $ids)->delete();
return redirect()->route('pplaces')->with('success', '削除しました。');
}
public function export() public function export()
{ {
$filename = '駐輪車室マスタ' . now()->format('YmdHis') . '.csv'; $headers = [
"Content-type" => "text/csv;charset=UTF-8",
$file = fopen($filename, 'w+'); "Content-Disposition" => "attachment; filename=Pplace.csv",
fwrite($file, "\xEF\xBB\xBF"); // BOM追加UTF-8 ];
$columns = ['駐輪車室ID', '番号', '備考', 'オペレータID'];
fputcsv($file, $columns);
$data = Pplace::all(); $data = Pplace::all();
$columns = ['ID', '番号', '備考', 'オペレータID'];
$filename = "Pplace.csv";
$file = fopen($filename, 'w+');
fputcsv($file, $columns);
foreach ($data as $item) { foreach ($data as $item) {
fputcsv($file, [ fputcsv($file, [
$item->pplace_id, $item->pplace_id,
@ -180,21 +135,14 @@ class PplaceController extends Controller
} }
fclose($file); fclose($file);
return Response::download($filename, $filename, $headers);
$headers = [
"Content-Type" => "text/csv; charset=UTF-8",
"Content-Disposition" => "attachment; filename={$filename}",
];
return response()->download($filename, $filename, $headers)->deleteFileAfterSend(true);
} }
public function import(Request $request) public function import(Request $request)
{ {
$file = $request->file('file'); $file = $request->file('file');
if (!$file) { if (!$file) {
return redirect()->route('pplaces')->with('error', 'CSVファイルを選択してください'); return redirect()->route('pplace')->with('error', 'CSVファイルを選択してください');
} }
$data = \App\Utils::csvToArray($file); $data = \App\Utils::csvToArray($file);
@ -213,10 +161,10 @@ class PplaceController extends Controller
]); ]);
} }
DB::commit(); DB::commit();
return redirect()->route('pplaces')->with('success', 'インポート成功'); return redirect()->route('pplace')->with('success', 'インポート成功');
} catch (\Exception $e) { } catch (\Exception $e) {
DB::rollBack(); DB::rollBack();
return redirect()->route('pplaces')->with('error', "{$record} : " . $e->getMessage()); return redirect()->route('pplace')->with('error', "{$record} : " . $e->getMessage());
} }
} }

View File

@ -5,17 +5,15 @@ namespace App\Http\Controllers\Admin;
use App\Http\Requests\PriceRequest; use App\Http\Requests\PriceRequest;
use App\Models\Park; use App\Models\Park;
use App\Models\Price; use App\Models\Price;
use App\Models\Pplace;
use App\Models\Psection; use App\Models\Psection;
use App\Models\Ptype; use App\Models\Ptype;
use App\Models\Usertype; use App\Models\Usertype;
use App\Models\Station;
use App\Models\Utils; use App\Models\Utils;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Response; use Response;
class PriceController extends Controller class PriceController extends Controller
{ {
@ -23,317 +21,226 @@ class PriceController extends Controller
{ {
$inputs = [ $inputs = [
'isExport' => 0, 'isExport' => 0,
'sort' => $request->input('sort', ''), // ソート対象カラム 'sort' => $request->input('sort', ''),
'sort_type' => $request->input('sort_type', ''), // 昇順/降順 'sort_type' => $request->input('sort_type', ''),
'page' => $request->get('page', 1), 'page' => $request->get('page', 1),
]; ];
// Price::search 内で orderBy を反映させる
$inputs['list'] = Price::search($inputs); $inputs['list'] = Price::search($inputs);
if ($inputs['list']->total() > 0 && $inputs['page'] > $inputs['list']->lastPage()) { if ($inputs['list']->total() > 0 && $inputs['page'] > $inputs['list']->lastPage()) {
return redirect()->route('prices'); return redirect()->route('prices');
} }
$dataList = $this->getDataDropList();
$inputs = array_merge($inputs, $dataList);
return view('admin.prices.list', $inputs); return view('admin.prices.list', $inputs);
} }
public function add(Request $request) public function add(Request $request)
{ {
if ($request->isMethod('get')) { $inputs = [
return view('admin.prices.add', array_merge( 'prine_name' => $request->input('prine_name'), // 商品名
$this->getDataDropList(), 'price_month' => $request->input('price_month',''), // 期間
[ 'park_id' => $request->input('park_name'), // 駐輪場
'record' => new Price(), 'psection_id' => $request->input('psection_subject'), // 車種区分
'isEdit' => false, 'price_ptypeid' => $request->input('ptype_subject'), // 駐輪分類
] 'user_categoryid' => $request->input('user_category_name'), // 利用者分類
'pplace_id' => $request->input('pplace_id'), // 駐車車室
'price' => $request->input('price'), // 駐輪料金(税込)
];
$dataList = $this->getDataDropList();
$inputs = array_merge($inputs, $dataList);
if ($request->isMethod('POST')) {
$type = false;
$validation = new PriceRequest();
$rules = $validation->rules();
$validator = Validator::make($request->all(), $rules, $validation->messages());
if (!$validator->fails()) {
\DB::transaction(function () use ($inputs, &$type) {
$new = new Price();
$new->fill($inputs);
if( $new->save()){
$type = true;
}
});
if ($type) {
$request->session()->flash('success', __('新しい成功を創造する。'));
return redirect()->route('prices');
} else {
$request->session()->flash('error', __('新しい作成に失敗しました'));
}
}else {
$inputs['errorMsg'] = $this->__buildErrorMessasges($validator);
}
}
return view('admin.prices.add', $inputs);
}
public function edit(Request $request, $pk ,$view=''){
$price = Price::getByPk($pk);
if (empty($pk) || empty($price)) {
abort('404');
}
$data = $price->getAttributes();
$dataList = $this->getDataDropList();
$data = array_merge($data, $dataList);
if ($request->isMethod('POST')) {
$type = false;
$validation = new PriceRequest();
$rules = $validation->rules();
$validator = Validator::make($request->all(), $rules, $validation->messages());
$requestAll = $request->all();
$requestAll['price_ptypeid]'] = $request->input('ptype_subject');
$requestAll['user_categoryid'] = $request->input('user_category_name'); // 利用者分類
$requestAll['psection_id'] = $request->input('psection_subject');
$requestAll['park_id'] = $request->input('park_name');
$data = array_merge($data, $requestAll);
if (!$validator->fails()) {
\DB::transaction(function () use ($data, &$type,$price) {
$price->fill($data);
$price->save();
$type = true;
});
if ($type) {
$request->session()->flash('success', __('更新に成功しました'));
return redirect()->route('prices');
} else {
$request->session()->flash('error', __('更新に失敗しました'));
}
}else {
$data['errorMsg'] = $this->__buildErrorMessasges($validator);
}
}
if ($view != '') {
return view($view, $data);
}
return view('admin.prices.edit', $data);
}
public function delete(Request $request)
{
$arr_pk = $request->get('pk');
if ($arr_pk) {
if (Price::deleteByPk($arr_pk)) {
return redirect()->route('prices')->with('success', __("削除成功。"));
} else {
return redirect()->route('prices')->with('error', __('削除に失敗しました。'));
}
}
return redirect()->route('prices')->with('error', __('削除するユーザーを選択してください。'));
}
public function export(Request $request)
{
$headers = array(
"Content-type" => "text/csv;charset=UTF-8",
'Content-Encoding: UTF-8',
"Content-Disposition" => "attachment; filename=file.csv",
"Pragma" => "no-cache",
"Cache-Control" => "must-revalidate, post-check=0, pre-check=0",
"Expires" => "0"
);
$inputs = [
'isMethodPost' => 0,
'isExport' => 1,
'sort' => $request->input('sort', ''),
'sort_type' => $request->input('sort_type', ''),
];
$dataExport = Price::search($inputs);
$columns = array(
__('駐車場所ID'),// 0
__('商品名'),// 1
__('期間'),// 2
__('駐輪場ID'),// 3
__('駐輪場名'),// 3
__('車種区分ID'),// 5
__('車種区分'),// 6
__('駐輪分類ID'),// 7
__('駐輪分類'),// 8
__('利用者分類ID'),// 9
__('利用者分類'),// 10
__('駐車車室ID'),//11
__('駐輪料金(税込)'),// 12
);
$filename = "駐輪場所、料金マスタ.csv";
$file = fopen($filename, 'w+');
fputcsv($file, $columns);
foreach ($dataExport as $items) {
fputcsv($file, array(
$items->price_parkplaceid,// 0
$items->prine_name, // 1
$items->price_month, // 2
$items->park_id, // 3
!empty($items->getPark())? $items->getPark()->park_name:'' ,// 4
$items->psection_id, // 5
!empty($items->getPSection())? $items->getPSection()->psection_subject:'',// 6
$items->price_ptypeid, // 7
!empty($items->getPType())? $items->getPType()->ptype_subject:'' ,// 8
$items->user_categoryid, //9
!empty($items->getUserType())? $items->getUserType()->print_name:'' ,//10
$items->pplace_id,// 11
$items->price, // 12
)); ));
} }
$request->merge([
'pplace_id' => mb_convert_kana($request->input('pplace_id'), 'n'),
]);
$validated = $this->validateRequest($request);
$created = false;
\DB::transaction(function () use ($validated, &$created) {
$price = new Price();
$price->fill($validated);
$created = $price->save();
});
return redirect()->route('prices')
->with($created ? 'success' : 'error', $created ? '登録しました。' : '登録に失敗しました。');
}
public function edit(Request $request, $id)
{
$price = Price::getByPk($id);
if (!$price) {
abort(404);
}
if ($request->isMethod('get')) {
return view('admin.prices.edit', array_merge(
$this->getDataDropList(),
[
'record' => $price,
'isEdit' => true,
]
));
}
$request->merge([
'pplace_id' => mb_convert_kana($request->input('pplace_id'), 'n'),
]);
$validated = $this->validateRequest($request, $id);
$updated = false;
\DB::transaction(function () use ($validated, &$updated, $price) {
$price->fill($validated);
$updated = $price->save();
});
return redirect()->route('prices')
->with($updated ? 'success' : 'error', $updated ? '更新しました。' : '更新に失敗しました。');
}
public function delete(Request $request, $id = null)
{
// 一覧画面checkbox で複数削除)
$ids = $request->input('pk');
// 編集画面(単体削除)
if ($id) {
$ids = [$id];
}
// 削除対象が空
if (empty($ids)) {
return redirect()->route('prices')->with('error', '削除対象が選択されていません。');
}
// 削除処理
Price::destroy($ids);
return redirect()->route('prices')->with('success', '削除しました。');
}
public static function deleteByPk($ids)
{
if (!is_array($ids)) {
$ids = [$ids];
}
return self::whereIn('price_parkplaceid', $ids)->delete();
}
public function export()
{
$filename = '駐輪場所、料金マスタ' . now()->format('YmdHis') . '.csv';
$file = fopen($filename, 'w+');
fwrite($file, "\xEF\xBB\xBF"); // BOM追加UTF-8
$columns = [
'駐輪場所ID',
'駐輪場ID',
'商品名',
'期間',
'利用者分類ID',
'駐輪料金(税込)',
'車種区分ID',
'駐輪分類ID',
'駐車車室ID',
];
fputcsv($file, $columns);
$data = Price::all();
foreach ($data as $item) {
fputcsv($file, [
$item->price_parkplaceid, // 駐輪場所ID
$item->park_id, // 駐輪場ID
optional($item->getUserType())->print_name, // 利用者分類名
$item->price_month, // 期間
optional($item->getUserType())->print_name, // 利用者分類ID
$item->price, // 駐輪料金(税込)
optional($item->getPSection())->psection_subject, // 車種区分名
optional($item->getPType())->ptype_subject, // 駐輪分類名
$item->pplace_id, // 駐車車室ID
]);
}
fclose($file); fclose($file);
return Response::download($filename, $filename, $headers);
$headers = [
"Content-Type" => "text/csv; charset=UTF-8",
"Content-Disposition" => "attachment; filename={$filename}",
];
return response()->download($filename, $filename, $headers)->deleteFileAfterSend(true);
} }
public function exportFiltered(Request $request)
{
$parkId = $request->input('park_id');
$filename = 'price_' . $parkId . '_' . now()->format('YmdHis') . '.csv';
// 一時ファイル作成
$file = fopen($filename, 'w+');
fwrite($file, "\xEF\xBB\xBF"); // UTF-8 BOM追加
// CSVヘッダ
$columns = [
'駐輪場所ID',
'商品名',
'期間',
'駐輪場ID',
'駐輪場名',
'車種区分ID',
'車種区分',
'駐輪分類ID',
'駐輪分類',
'利用者分類ID',
'利用者分類',
'駐輪車室ID',
'駐輪料金(税込)',
];
fputcsv($file, $columns);
// データ取得選択した駐輪場IDに限定
$data = \App\Models\Price::where('price_parkplaceid', $parkId)->get();
foreach ($data as $item) {
fputcsv($file, [
$item->price_parkplaceid, // 駐輪場所ID
$item->prine_name, // 商品名
$item->price_term ?? $item->price_month, // 期間
$item->park_id, // 駐輪場ID
optional($item->getPark())->park_name, // 駐輪場名
$item->psection_id, // 車種区分ID
optional($item->getPSection())->psection_subject, // 車種区分
$item->price_ptypeid, // 駐輪分類ID
optional($item->getPType())->ptype_subject, // 駐輪分類
$item->user_categoryid, // 利用者分類ID
optional($item->getUserType())->print_name, // 利用者分類
$item->pplace_id, // 駐輪車室ID
$item->price_taxin ?? $item->price, // 駐輪料金(税込)
]);
}
fclose($file);
// ヘッダ設定
$headers = [
"Content-Type" => "text/csv; charset=UTF-8",
"Content-Disposition" => "attachment; filename=\"{$filename}\"",
];
// ダウンロード後にファイル削除
return response()->download($filename, $filename, $headers)->deleteFileAfterSend(true);
}
public function import(Request $request) public function import(Request $request)
{ {
$file = $request->file('file'); $file = $request->file('file');
if (empty($file)) { if(!empty($file)){
return redirect()->route('prices')->with('error', __('CSVファイルを選択してください。'));
}
$data = Utils::csvToArray($file); $data = Utils::csvToArray($file);
$type = true; $type = 1;
$msg = ''; $msg = '';
$record = 0; $record = 0;
DB::beginTransaction(); DB::beginTransaction();
try { try {
Price::query()->delete();
$col = 13; // CSV 項目数 $col = 13;
foreach ($data as $key => $items) { foreach ($data as $key => $items) {
$record = $key + 2; // エラー行番号(ヘッダ行を考慮) $record = $key + 2;
if (count($items) == $col) {
// 項目数チェック $row = new Price();
if (count($items) != $col) { $row->price_parkplaceid = $items[0];
$type = false; $row->prine_name = $items[1];
$msg = "行:{$record} 列数が一致しません。"; $row->price_month = $items[2];
$row->park_id = $items[3];
$row->psection_id = $items[5];
$row->price_ptypeid = $items[7];
$row->user_categoryid = $items[9];
$row->pplace_id = $items[11];
$row->price = $items[12];
if (!$row->save()) {
$type = 0;
$msg = '行:record型が一致しません。';
break; break;
} }
} else {
// 必須チェック $type = 0;
if (empty($items[0])) { $type = false; $msg = "行:{$record} 駐車場所IDが未設定です。"; break; } $msg = '行:record列数が一致しません。';
if (empty($items[1])) { $type = false; $msg = "行:{$record} 商品名が未設定です。"; break; } break;
if (empty($items[2])) { $type = false; $msg = "行:{$record} 期間が未設定です。"; break; }
if (empty($items[3])) { $type = false; $msg = "行:{$record} 駐輪場IDが未設定です。"; break; }
if (empty($items[5])) { $type = false; $msg = "行:{$record} 車種区分IDが未設定です。"; break; }
if (empty($items[7])) { $type = false; $msg = "行:{$record} 駐輪分類IDが未設定です。"; break; }
if (empty($items[9])) { $type = false; $msg = "行:{$record} 利用者分類IDが未設定です。"; break; }
if (empty($items[11])) { $type = false; $msg = "行:{$record} 駐車車室IDが未設定です。"; break; }
if (empty($items[12])) { $type = false; $msg = "行:{$record} 駐輪料金が未設定です。"; break; }
// マスタ存在チェック
if (!Park::where('park_id', $items[3])->exists()) {
$type = false; $msg = "行:{$record} 駐輪場IDが存在しません。"; break;
} }
if (!Psection::where('psection_id', $items[5])->exists()) {
$type = false; $msg = "行:{$record} 車種区分IDが存在しません。"; break;
}
if (!Ptype::where('ptype_id', $items[7])->exists()) {
$type = false; $msg = "行:{$record} 駐輪分類IDが存在しません。"; break;
}
if (!Usertype::where('user_categoryid', $items[9])->exists()) {
$type = false; $msg = "行:{$record} 利用者分類IDが存在しません。"; break;
}
// TODO: 駐車車室ID チェックpplace_id
if (!Pplace::where('pplace_id', $items[11])->exists()) {
$type = false; $msg = "行:{$record} 駐車車室IDが存在しません。"; break;
}
// 保存(存在すれば更新、なければ新規作成)
Price::updateOrCreate(
['price_parkplaceid' => $items[0]], // 主キー条件(存在チェック)
[
'prine_name' => $items[1],
'price_month' => $items[2],
'park_id' => $items[3],
'psection_id' => $items[5],
'price_ptypeid' => $items[7],
'user_categoryid' => $items[9],
'pplace_id' => $items[11],
'price' => $items[12],
'operator_id' => auth()->user()->operator_id ?? null, // オプション
'updated_at' => now(),
'created_at' => now(),
]
);
} }
} catch (\Exception $e) { } catch (\Exception $e) {
$type = false; $msg = '行:record型が一致しません。';
$msg = "行:{$record} 予期せぬエラー: ".$e->getMessage(); $type = 0;
} }
if ($type) { if ($type) {
DB::commit(); DB::commit();
return redirect()->route('prices')->with('success', __('インポートが正常に完了しました。')); return redirect()->route('prices')->with('success', __('輸入成功'));
} else { } else {
DB::rollBack(); DB::rollBack();
return redirect()->route('prices')->with('error', $msg); return redirect()->route('prices')->with('error', __($msg, ['record' => $record]));
}
} else {
return redirect()->route('prices')->with('error', __('あなたはcsvファイルを選択していません。'));
} }
} }
public function info(Request $request, $id) public function info(Request $request, $id)
{ {
return $this->edit($request, $id, 'admin.prices.info'); return $this->edit($request, $id, 'admin.prices.info');
@ -345,33 +252,8 @@ class PriceController extends Controller
$data['psections'] = Psection::getList() ; $data['psections'] = Psection::getList() ;
$data['ptypes'] = Ptype::getList() ; $data['ptypes'] = Ptype::getList() ;
$data['userTypes'] = Usertype::getList() ; $data['userTypes'] = Usertype::getList() ;
$data['pplaces'] = Pplace::getList() ;
return $data; return $data;
} }
/**
* Price バリデーション共通
*/
private function validateRequest(Request $request): array
{
return $request->validate([
'prine_name' => 'required|string|max:255',
'price_month' => 'required|int',
'park_id' => 'required|int',
'psection_id' => 'required|int',
'price_ptypeid' => 'required|int',
'user_categoryid' => 'required|int',
'pplace_id' => 'nullable|int',
'park_number' => 'nullable|int',
'park_standard' => 'nullable|int',
'park_limit' => 'nullable|int',
'price' => 'required|numeric',
'operator_id' => 'nullable|int',
]);
}
} }

View File

@ -32,26 +32,16 @@ class PriceListController extends Controller
]; ];
if ($parkId) { if ($parkId) {
// price_a に必要なマスタ JOIN // parkとprice_aをJOIN
$aRows = \DB::table('price_a') $aRows = \DB::table('price_a')
->join('park', 'park.park_id', '=', 'price_a.park_id') ->join('park', 'park.park_id', '=', 'price_a.park_id')
->leftJoin('ptype', 'price_a.ptype_id', '=', 'ptype.ptype_id')
->leftJoin('usertype', 'price_a.user_categoryid', '=', 'usertype.user_categoryid')
->leftJoin('pplace', 'price_a.pplace_id', '=', 'pplace.pplace_id')
->where('price_a.park_id', $parkId) ->where('price_a.park_id', $parkId)
->select([ ->select('price_a.*') // 必要ならpark.*も
'price_a.*',
'ptype.ptype_subject',
'usertype.usertype_subject1',
'usertype.usertype_subject2',
'usertype.usertype_subject3',
'pplace.pplace_number'
])
->get(); ->get();
$aGrouped = $this->groupPriceRows($aRows); $aGrouped = $this->groupPriceRows($aRows);
$masterList[0]['groups'] = $aGrouped;
$masterList[0]['groups'] = $aGrouped;
// マスターBも同様に取得・整形する場合はここに追加 // マスターBも同様に取得・整形する場合はここに追加
} }
@ -70,103 +60,85 @@ class PriceListController extends Controller
$result = []; $result = [];
foreach ($rows as $row) { foreach ($rows as $row) {
// グループキーは分類ID+ユーザ分類ID+駐輪場ID // グループキーは分類ID+ユーザ分類ID+駐輪場ID
$key = $row->ptype_id . '-' . $row->user_categoryid . '-' . $row->park_id; $key = $row->price_ptypeid . '-' . $row->user_categoryid . '-' . $row->park_id;
if (!isset($result[$key])) { if (!isset($result[$key])) {
$result[$key] = [ $result[$key] = [
'id' => $row->price_parkplaceid, 'id' => $row->price_parkplaceid,
'classification' => $row->ptype_subject ?? '', 'classification' => $row->price_ptypeid,
'room_number' => $row->pplace_number ?? '', 'room_number' => '', // 必要ならpplace_id等をセット
'category1' => $row->usertype_subject1 ?? '', 'category1' => $row->prine_name ?? '',
'category2' => $row->usertype_subject2 ?? '', 'category2' => '',
'category3' => $row->usertype_subject3 ?? '', 'category3' => '',
'bike_1m' => '', 'bike_2m' => '', 'bike_3m' => '', 'bike_6m' => '', 'bike_12m' => '', 'bike_1m' => '',
'moped_1m' => '', 'moped_2m' => '', 'moped_3m' => '', 'moped_6m' => '', 'moped_12m' => '', 'bike_2m' => '',
'motorcycle_1m' => '', 'motorcycle_2m' => '', 'motorcycle_3m' => '', 'motorcycle_6m' => '', 'motorcycle_12m' => '', 'bike_3m' => '',
'car_1m' => '', 'car_2m' => '', 'car_3m' => '', 'car_6m' => '', 'car_12m' => '', 'bike_6m' => '',
'bike_12m' => '',
// 必要なら原付・自動二輪も同様に追加
]; ];
} }
$month = $row->price_month; // 月数ごとに金額をセット
$price = $row->price; if ($row->price_month == 1) {
switch ($row->psection_id) { $result[$key]['bike_1m'] = $row->price;
case 1: } elseif ($row->price_month == 2) {
$result[$key]["bike_{$month}m"] = $price; $result[$key]['bike_2m'] = $row->price;
break; } elseif ($row->price_month == 3) {
case 2: $result[$key]['bike_3m'] = $row->price;
$result[$key]["moped_{$month}m"] = $price; } elseif ($row->price_month == 6) {
break; $result[$key]['bike_6m'] = $row->price;
case 3: } elseif ($row->price_month == 12) {
$result[$key]["motorcycle_{$month}m"] = $price; $result[$key]['bike_12m'] = $row->price;
break;
case 4:
$result[$key]["car_{$month}m"] = $price;
break;
} }
} }
return array_values($result); return array_values($result);
} }
public function update(Request $request) public function update(Request $request)
{ {
foreach ($request->input('rows', []) as $row) { foreach ($request->input('rows', []) as $row) {
$id = $row['id'] ?? null; $id = $row['id'] ?? null;
if (!$id) continue; if (!$id) continue;
$vehicleTypes = [ // 更新対象の月リスト
'bike' => 1, $months = [
'moped' => 2, 'bike_1m' => 1,
'motorcycle' => 3, 'bike_2m' => 2,
'car' => 4, 'bike_3m' => 3,
'bike_6m' => 6,
'bike_12m' => 12,
]; ];
$months = [1, 2, 3, 6, 12]; foreach ($months as $field => $month) {
foreach ($vehicleTypes as $prefix => $psectionId) {
foreach ($months as $month) {
$field = "{$prefix}_{$month}m";
if (isset($row[$field])) { if (isset($row[$field])) {
$value = $row[$field]; // price_aから該当レコードを取得
$item = \App\Models\PriceA::where('price_parkplaceid', $id)
// バリデーション:空欄はスキップ
if (!ctype_digit((string)$value)) {
return back()->withErrors([
"{$field}" => "金額は整数で入力してください。"
]);
}
// バリデーション:最大値を超えないこと
if ((int)$value > 9999999999) {
return back()->withErrors([
"{$field}" => "金額は最大 9,999,999,999 までです。"
]);
}
$item = PriceA::where('price_parkplaceid', $id)
->where('price_month', $month) ->where('price_month', $month)
->where('psection_id', $psectionId)
->first(); ->first();
if ($item) { if ($item) {
$item->price = $value; $item->price = $row[$field];
$item->save(); $item->save();
} }
} }
} }
} // 原付・自動二輪も同様に必要なら追加
} }
return back()->with('success', '金額を更新しました'); return back()->with('success', '金額を更新しました');
} }
public function insert(Request $request) public function insert(Request $request)
{ {
// 例bike_2m2ヶ月だけ新規追加する場合
if ($request->filled('bike_2m')) { if ($request->filled('bike_2m')) {
$row = new PriceA(); $row = new \App\Models\PriceA();
$row->park_id = $request->input('park_id'); $row->park_id = $request->input('park_id'); // 必要に応じて
$row->price = $request->input('bike_2m'); $row->price = $request->input('bike_2m');
$row->price_month = 2; $row->price_month = 2;
$row->psection_id = 1; // 自転車 // 他の必要なカラムもセット
$row->save(); $row->save();
} }
// 他の月も同様に必要なら追加
return back()->with('success', '金額を追加しました'); return back()->with('success', '金額を追加しました');
} }
} }

View File

@ -25,7 +25,7 @@ class PrintAreaController extends Controller
]); ]);
} }
// 新規 // 新規登録
public function add(Request $request) public function add(Request $request)
{ {
if ($request->isMethod('post')) { if ($request->isMethod('post')) {
@ -37,7 +37,7 @@ class PrintAreaController extends Controller
$validated['operator_id'] = auth()->id(); // 現在のログインユーザーを記録 $validated['operator_id'] = auth()->id(); // 現在のログインユーザーを記録
PrintArea::create($validated); PrintArea::create($validated);
return redirect()->route('print_areas')->with('success', '登録しました'); return redirect()->route('print_areas')->with('success', '登録しました');
} }
$parks = Park::pluck('park_name', 'park_id'); $parks = Park::pluck('park_name', 'park_id');
@ -58,7 +58,7 @@ class PrintAreaController extends Controller
$validated['operator_id'] = auth()->id(); // 更新者を記録 $validated['operator_id'] = auth()->id(); // 更新者を記録
$record->update($validated); $record->update($validated);
return redirect()->route('print_areas')->with('success', '更新しました'); return redirect()->route('print_areas')->with('success', '更新しました');
} }
$parks = Park::pluck('park_name', 'park_id'); $parks = Park::pluck('park_name', 'park_id');
@ -72,67 +72,24 @@ class PrintAreaController extends Controller
return view('admin.print_areas.info', compact('record')); return view('admin.print_areas.info', compact('record'));
} }
/** // 削除(複数可)
* 印刷範囲マスタ削除処理 public function delete(Request $request)
*/
public function delete(Request $request, $id = null)
{ {
// 一覧画面checkboxで複数削除 if ($request->has('pk')) {
$ids = $request->input('pk'); PrintArea::destroy($request->input('pk'));
return redirect()->route('print_areas')->with('success', '削除しました');
// 編集画面(単体削除)
if ($id) {
$ids = [$id];
} }
// 削除対象が空 return redirect()->route('print_areas')->with('error', '削除対象が見つかりません');
if (empty($ids)) {
return redirect()
->route('print_areas')
->with('error', '削除対象が選択されていません。');
}
// バリデーション:配列 or 単一でも整数確認
$request->validate([
'pk' => 'nullable',
'pk.*' => 'integer',
]);
try {
// 削除処理
$deleted = PrintArea::destroy($ids);
if ($deleted > 0) {
return redirect()
->route('print_areas')
->with('success', '削除しました。');
} else {
return redirect()
->route('print_areas')
->with('error', '削除に失敗しました。');
}
} catch (\Exception $e) {
\Log::error('印刷範囲削除エラー: ' . $e->getMessage());
return redirect()
->route('print_areas')
->with('error', '削除中にエラーが発生しました。');
}
} }
// CSVエクスポート
public function export(Request $request) public function export(Request $request)
{ {
// ファイル名を日本語付きで指定Excelで問題なく開けるようにUTF-8にBOMも付加 $filename = 'print_areas_' . now()->format('Ymd_His') . '.csv';
$filename = 'シール印刷範囲マスタ' . now()->format('YmdHis') . '.csv';
$data = PrintArea::with('park')->get(); $data = PrintArea::with('park')->get();
// UTF-8 BOM (Excel用)
$bom = "\xEF\xBB\xBF";
// CSVヘッダー
$csv = implode(",", ['印刷範囲ID', '印刷範囲名', '駐輪場ID', '駐輪場名']) . "\n"; $csv = implode(",", ['印刷範囲ID', '印刷範囲名', '駐輪場ID', '駐輪場名']) . "\n";
foreach ($data as $item) { foreach ($data as $item) {
$csv .= implode(",", [ $csv .= implode(",", [
$item->print_area_id, $item->print_area_id,
@ -142,13 +99,11 @@ class PrintAreaController extends Controller
]) . "\n"; ]) . "\n";
} }
return response($bom . $csv) return response($csv)
->header('Content-Type', 'text/csv; charset=UTF-8') ->header('Content-Type', 'text/csv')
// filename* にすれば日本語名も安全に動作 ->header('Content-Disposition', "attachment; filename=$filename");
->header('Content-Disposition', "attachment; filename*=UTF-8''" . rawurlencode($filename));
} }
// CSVインポート // CSVインポート
public function import(Request $request) public function import(Request $request)
{ {

View File

@ -11,118 +11,63 @@ class PsectionController extends Controller
// 一覧画面 // 一覧画面
public function list(Request $request) public function list(Request $request)
{ {
$inputs = $request->all(); // ソート順
// ソート可能なカラム $sort = $request->input('sort', 'psection_id');
$allowedSortColumns = ['psection_id', 'psection_subject']; $sort_type = $request->input('sort_type', 'asc');
// ソート情報の取得 $query = Psection::query();
$sortColumn = $inputs['sort'] ?? 'psection_id'; if (in_array($sort, ['psection_id', 'psection_subject'])) {
$sortType = strtolower($inputs['sort_type'] ?? 'asc'); $query->orderBy($sort, $sort_type);
$query = \App\Models\Psection::query();
if (in_array($sortColumn, $allowedSortColumns, true)) {
if (!in_array($sortType, ['asc', 'desc'], true)) {
$sortType = 'asc';
}
$query->orderBy($sortColumn, $sortType);
} }
// ページネーション20件 $list = $query->get();
$list = $query->paginate(20);
return view('admin.psection.list', [ return view('admin.psection.list', compact('list', 'sort', 'sort_type'));
'list' => $list,
'sort' => $sortColumn,
'sort_type' => $sortType,
]);
} }
/** // 新規追加
* 車種区分マスタ:新規登録(画面/処理)
*/
public function add(Request $request) public function add(Request $request)
{ {
if ($request->isMethod('get')) { if ($request->isMethod('post')) {
// GET新規画面を表示
return view('admin.psection.add', [
'isEdit' => false,
'record' => new Psection(),
]);
}
// POSTバリデーション
$validated = $request->validate([ $validated = $request->validate([
// 'psection_id' は自動採番 'psection_id' => 'required|integer|unique:psection,psection_id',
'psection_subject' => 'required|string|max:255', 'psection_subject' => 'required|string|max:255',
]); ]);
// 登録処理
Psection::create($validated); Psection::create($validated);
return redirect()->route('psection')->with('success', '車種区分を追加しました');
// 完了メッセージ+一覧へ戻る }
return redirect()->route('psections')->with('success', '登録しました。'); return view('admin.psection.add');
} }
// 編集
/**
* 車種区分マスタ:編集(画面/処理)
*/
public function edit(Request $request, $id) public function edit(Request $request, $id)
{ {
// 主キーで検索見つからない場合は404 $psection = Psection::findOrFail($id);
$record = Psection::findOrFail($id); if ($request->isMethod('post')) {
if ($request->isMethod('get')) {
// 編集画面表示
return view('admin.psection.edit', [
'isEdit' => true, // ← ★ Blade 側のフォームで新規/編集を判定するため
'record' => $record, // ← ★ _form.blade.php で使用する
]);
}
// POST時バリデーション
$validated = $request->validate([ $validated = $request->validate([
'psection_subject' => 'required|string|max:255', 'psection_subject' => 'required|string|max:255',
]); ]);
$psection->update($validated);
// データ更新 return redirect()->route('psection')->with('success', '車種区分を更新しました');
$record->update($validated); }
return view('admin.psection.edit', compact('psection'));
// 成功メッセージ & リダイレクト
return redirect()->route('psections')->with('success', '更新しました。');
} }
/** // 詳細(info)
* 削除(単一/複数対応) public function info(Request $request, $id)
*/ {
$psection = Psection::findOrFail($id);
return view('admin.psection.info', compact('psection'));
}
// 削除
public function delete(Request $request) public function delete(Request $request)
{ {
$ids = []; $ids = $request->input('pk', []);
if (!empty($ids)) {
// 単一削除(編集画面などからの削除ボタン)
if ($request->filled('id')) {
$ids[] = (int) $request->input('id');
}
// 一覧画面からの複数削除チェックボックス対応
if (is_array($request->input('pk'))) {
$ids = array_merge($ids, $request->input('pk'));
}
// 重複削除・無効値除去
$ids = array_values(array_unique(array_map('intval', $ids)));
// 削除対象がない場合
if (empty($ids)) {
return back()->with('error', '削除対象が選択されていません。');
}
// 削除実行
Psection::whereIn('psection_id', $ids)->delete(); Psection::whereIn('psection_id', $ids)->delete();
return redirect()->route('psection')->with('success', '削除しました');
// 完了メッセージ+リダイレクト }
return redirect()->route('psections')->with('success', '削除しました。'); return redirect()->route('psection')->with('error', '削除対象を選択してください');
} }
} }

View File

@ -14,156 +14,125 @@ class PtypeController extends Controller
{ {
public function list(Request $request) public function list(Request $request)
{ {
// 受け取る入力値
$inputs = [ $inputs = [
'isMethodPost' => $request->isMethod('post') ? 1 : 0, 'isMethodPost' => 0,
'isExport' => 0, 'isExport' => 0,
'sort' => $request->input('sort', 'ptype_id'), // デフォルト: ID 'sort' => $request->input('sort', ''),
'sort_type' => strtolower($request->input('sort_type', 'asc')), 'sort_type' => $request->input('sort_type', ''),
'page' => $request->get('page', 1), 'page' => $request->get('page', 1),
]; ];
$inputs['isMethodPost'] = $request->isMethod('post');
// 許可するソート可能カラム $inputs['list'] = Ptype::search($inputs);
$allowedSortColumns = [
'ptype_id',
'ptype_subject',
'floor_sort',
'created_at',
];
// 検索クエリ作成
$query = \App\Models\Ptype::query();
// ソート処理
if (in_array($inputs['sort'], $allowedSortColumns, true)) {
$sortType = in_array($inputs['sort_type'], ['asc', 'desc'], true)
? $inputs['sort_type']
: 'asc';
$query->orderBy($inputs['sort'], $sortType);
} else {
$query->orderBy('ptype_id', 'asc');
}
// ページネーション
$inputs['list'] = $query->paginate(20);
// ページが超過している場合リダイレクト
if ($inputs['list']->total() > 0 && $inputs['page'] > $inputs['list']->lastPage()) { if ($inputs['list']->total() > 0 && $inputs['page'] > $inputs['list']->lastPage()) {
return redirect()->route('ptypes'); return redirect()->route('ptypes');
} }
// 画面へ
return view('admin.ptypes.list', $inputs); return view('admin.ptypes.list', $inputs);
} }
/**
* 新規登録(画面/処理)
*/
public function add(Request $request) public function add(Request $request)
{ {
if ($request->isMethod('get')) { $inputs = [
// 新規時は空のレコードを用意してフォーム描画 //TODO 駐輪分類ID not found in database specs
return view('admin.ptypes.add', [ 'ptype_subject' => $request->input('ptype_subject'), // 駐輪分類名
'isEdit' => false, 'ptype_remarks' => $request->input('ptype_remarks'), // 備考
'record' => new Ptype(), // ← ★ Blade側で record-> が使える ];
]);
}
// POST時バリデーション if ($request->isMethod('POST')) {
$rules = [ $rules = [
'ptype_subject' => 'required|string|max:255', 'ptype_subject' => 'required|string|max:255',
'ptype_sort' => 'nullable|integer',
'ptype_remarks' => 'nullable|string|max:255', 'ptype_remarks' => 'nullable|string|max:255',
]; ];
$messages = [ $messages = [
'ptype_subject.required' => '駐輪分類名は必須です。', 'ptype_subject.required' => '駐輪分類名は必須です。',
]; ];
$validator = Validator::make($request->all(), $rules, $messages); $validator = Validator::make($request->all(), $rules, $messages);
if (!$validator->fails()) {
if ($validator->fails()) { \DB::transaction(function () use ($inputs, &$type) {
return redirect()->back()->withErrors($validator)->withInput();
}
DB::transaction(function () use ($request) {
$new = new Ptype(); $new = new Ptype();
$new->fill($request->only(['ptype_subject', 'ptype_remarks'])); $new->fill($inputs);
$new->save(); if ($new->save()) {
$type = true;
}
}); });
if ($type) {
return redirect()->route('ptypes')->with('success', '登録しました。'); $request->session()->flash('success', __('データ新規作成しました。'));
return redirect()->route('ptypes');
} else {
$request->session()->flash('error', __('新規作成に失敗しました'));
}
} else {
$inputs['errorMsg'] = $this->__buildErrorMessasges($validator);
}
} }
/** return view('admin.ptypes.add', $inputs);
* 編集(画面/処理) }
*/
public function edit(Request $request, $id) public function edit(Request $request, $pk, $view = '')
{ {
// 該当データ取得 $ptype = Ptype::getByPk($pk);
$record = Ptype::find($id); if (empty($pk) || empty($ptype)) {
if (!$record) { abort('404');
abort(404);
} }
$data = $ptype->getAttributes();
if ($request->isMethod('get')) { $dataList = $this->getDataDropList();
// 編集画面表示 $data = array_merge($data, $dataList);
return view('admin.ptypes.edit', [ if ($request->isMethod('POST')) {
'isEdit' => true, $type = false;
'record' => $record, // ここを修正
]);
}
// POST時バリデーション
$rules = [ $rules = [
'ptype_subject' => 'required|string|max:255', 'ptype_subject' => 'required|string|max:255',
'floor_sort' => 'nullable|string|max:50', 'ptype_sort' => 'nullable|integer',
'ptype_remarks' => 'nullable|string|max:255', 'ptype_remarks' => 'nullable|string|max:255',
]; ];
$messages = [ $messages = [
'ptype_subject.required' => '駐輪分類名は必須です。', 'ptype_subject.required' => '駐輪分類名は必須です。',
]; ];
$validator = Validator::make($request->all(), $rules, $messages); $validator = Validator::make($request->all(), $rules, $messages);
$requestAll = $request->all();
if ($validator->fails()) { $data = array_merge($data, $requestAll);
return redirect()->back()->withErrors($validator)->withInput(); if (!$validator->fails()) {
} \DB::transaction(function () use ($data, &$type, $ptype) {
$ptype->fill($data);
DB::transaction(function () use ($request, $record) { $ptype->save();
$record->fill($request->only(['ptype_subject','floor_sort', 'ptype_remarks'])); $type = true;
$record->save();
}); });
if ($type) {
return redirect()->route('ptypes')->with('success', '更新しました。'); $request->session()->flash('success', __('更新に成功しました'));
return redirect()->route('ptypes');
} else {
$request->session()->flash('error', __('更新に失敗しました'));
}
} else {
$data['errorMsg'] = $this->__buildErrorMessasges($validator);
}
}
if ($view != '') {
return view($view, $data);
}
return view('admin.ptypes.edit', $data);
} }
/**
* 削除(単一/複数対応)
*/
public function delete(Request $request) public function delete(Request $request)
{ {
$ids = []; $arr_pk = $request->get('pk');
if ($arr_pk) {
if ($request->filled('id')) { if (Ptype::deleteByPk($arr_pk)) {
$ids[] = (int) $request->input('id'); return redirect()->route('ptypes')->with('success', __("削除が完了しました。"));
} else {
return redirect()->route('ptypes')->with('error', __('削除に失敗しました。'));
}
}
return redirect()->route('ptypes')->with('error', __('削除するデータを選択してください。'));
} }
if (is_array($request->input('pk'))) {
$ids = array_merge($ids, $request->input('pk'));
}
$ids = array_values(array_unique(array_map('intval', $ids))); public function info(Request $request, $id)
{
if (empty($ids)) { return $this->edit($request, $id, 'admin.ptypes.info');
return back()->with('error', '削除対象が選択されていません。');
}
Ptype::whereIn('ptype_id', $ids)->delete();
return redirect()->route('ptypes')->with('success', '削除しました。');
} }
public function getDataDropList() public function getDataDropList()

View File

@ -1,120 +0,0 @@
<?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();
}
}
}

View File

@ -5,91 +5,9 @@ namespace App\Http\Controllers\Admin;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use App\Models\Park;
use Illuminate\Support\Carbon;
class RegularContractController class RegularContractController
{ {
/**
* 利用者分類選択肢user_categoryid 昇順)
*/
private function buildUsertypeOptions(): array
{
return DB::table('usertype as t')
->join('regular_contract as rc', 'rc.user_categoryid', '=', 't.user_categoryid')
->select('t.user_categoryid', 't.usertype_subject1', 't.usertype_subject2', 't.usertype_subject3')
->groupBy('t.user_categoryid', 't.usertype_subject1', 't.usertype_subject2', 't.usertype_subject3')
->orderBy('t.user_categoryid', 'asc')
->get()
->mapWithKeys(function ($row) {
$label = collect([
$row->usertype_subject1 ?? '',
$row->usertype_subject2 ?? '',
$row->usertype_subject3 ?? '',
])->filter(fn ($v) => $v !== '')->implode('/');
return [$row->user_categoryid => $label !== '' ? $label : (string) $row->user_categoryid];
})
->toArray();
}
private function buildParkOptions(): array
{
return Park::query()
->join('regular_contract as rc', 'rc.park_id', '=', 'park.park_id')
->select('park.park_id', 'park.park_name')
->groupBy('park.park_id', 'park.park_name')
->orderBy('park.park_id', 'asc')
->get()
->mapWithKeys(fn ($park) => [
$park->park_id => $park->park_name ?: (string) $park->park_id,
])
->toArray();
}
/**
* datetime-local から受け取った値を Y-m-d H:i:s へ正規化
*/
private function normalizeDateTimeInput(?string $value, bool $endOfMinute = false): ?string
{
if ($value === null) {
return null;
}
$value = trim($value);
if ($value === '') {
return null;
}
$value = str_replace('T', ' ', $value);
if (strlen($value) === 16) {
$value .= ':00';
}
try {
$dt = Carbon::parse($value);
if ($endOfMinute) {
$dt = $dt->endOfMinute();
}
return $dt->format('Y-m-d H:i:s');
} catch (\Throwable $e) {
return null;
}
}
/**
* 名寄フリガナ検索用:全角カナへ統一し空白除去
*/
private function normalizePhoneticKeyword(?string $value): ?string
{
if ($value === null) {
return null;
}
$value = trim((string) $value);
if ($value === '') {
return null;
}
$value = mb_convert_kana($value, 'KVCS');
return str_replace([' ', ' '], '', $value);
}
/** /**
* 定期契約一覧 * 定期契約一覧
* - ベース表: regular_contractrc * - ベース表: regular_contractrc
@ -98,86 +16,74 @@ class RegularContractController
*/ */
public function list(Request $request) public function list(Request $request)
{ {
if ($request->isMethod('post')) { // ===== ソート(既定: contract_id DESC=====
$postParams = $request->except(['_token']); $sort = $request->input('sort', 'contract_id');
$queryParams = $request->query(); $sortType = strtolower($request->input('sort_type', 'desc')) === 'asc' ? 'asc' : 'desc';
return redirect()->route('regularcontracts', array_merge($queryParams, $postParams));
}
$params = $request->query();
// ===== ソート(既定: contract_id ASC=====
$sort = $params['sort'] ?? 'contract_id';
$sortType = strtolower($params['sort_type'] ?? 'asc') === 'desc' ? 'desc' : 'asc';
// ===== 絞り込み(テキスト系)===== // ===== 絞り込み(テキスト系)=====
$contract_qr_id = trim((string) ($params['contract_qr_id'] ?? '')); // フォームの name 属性と完全一致させる&既定値は空文字にして Blade が未定義にならないようにする
$user_id = trim((string) ($params['user_id'] ?? '')); $contract_qr_id = trim((string) $request->input('contract_qr_id', ''));
$user_tag_serial = trim((string) ($params['user_tag_serial'] ?? '')); $user_id = trim((string) $request->input('user_id', ''));
$park_id = trim((string) ($params['park_id'] ?? '')); $park_id = trim((string) $request->input('park_id', ''));
$selectedParkId = trim((string) ($params['selected_park_id'] ?? '')); $user_phonetic = trim((string) $request->input('user_phonetic', '')); // フリガナ
$user_phonetic = trim((string) ($params['user_phonetic'] ?? '')); $phone = trim((string) $request->input('phone', '')); // 電話(携帯/自宅)
$phone = trim((string) ($params['phone'] ?? '')); $email = trim((string) $request->input('email', '')); // メール
$email = trim((string) ($params['email'] ?? '')); $usertype_name_kw = trim((string) $request->input('usertype_name', '')); // 利用者分類名
$user_categoryid = trim((string) ($params['user_categoryid'] ?? '')); $park_name_kw = trim((string) $request->input('park_name', '')); // 駐輪場名
$park_name_kw = trim((string) ($params['park_name'] ?? ''));
$zone_keyword = trim((string) ($params['zone_keyword'] ?? ''));
$zone_name = trim((string) ($params['zone_name'] ?? ''));
$merge_phonetic_input = $params['merge_phonetic'] ?? '';
$merge_phonetic = trim((string) $merge_phonetic_input);
$merge_phonetic_normalized = $this->normalizePhoneticKeyword($merge_phonetic);
$has_address = $params['has_address'] ?? '';
$workRecordFilter = (string) ($params['work_record'] ?? '0');
if (!in_array($workRecordFilter, ['0', '1', '2'], true)) {
$workRecordFilter = '0';
}
// ===== 絞り込み(日付範囲)===== // ===== 絞り込み(日付範囲)=====
$reserve_from = $params['reserve_date_from'] ?? ''; $reserve_from = $request->input('reserve_date_from', '');
$reserve_to = $params['reserve_date_to'] ?? ''; $reserve_to = $request->input('reserve_date_to', '');
$created_from = $params['contract_created_from'] ?? ''; $created_from = $request->input('contract_created_from', '');
$created_to = $params['contract_created_to'] ?? ''; $created_to = $request->input('contract_created_to', '');
$updated_from = $params['contract_updated_from'] ?? ''; $updated_from = $request->input('contract_updated_from', '');
$updated_to = $params['contract_updated_to'] ?? ''; $updated_to = $request->input('contract_updated_to', '');
$canceled_from = $params['contract_canceled_from'] ?? ''; $canceled_from = $request->input('contract_canceled_from', '');
$canceled_to = $params['contract_canceled_to'] ?? ''; $canceled_to = $request->input('contract_canceled_to', '');
$receipt_delivery_from = $params['receipt_delivery_from'] ?? '';
$receipt_delivery_to = $params['receipt_delivery_to'] ?? '';
$contract_valid_months = $params['contract_valid_months'] ?? '';
// ===== 列挙(全て/0/1===== // ===== 列挙(全て/0/1=====
$contract_flag = $params['contract_flag'] ?? ''; $contract_flag = $request->input('contract_flag', '');
$contract_permission = $params['contract_permission'] ?? ''; $contract_permission = $request->input('contract_permission', '');
$tag_qr_flag = $params['tag_qr_flag'] ?? ''; $tag_qr_flag = $request->input('tag_qr_flag', '');
$updateFlagFilter = (string) ($params['update_flag'] ?? '0'); $update_flag = $request->input('update_flag', '');
if (!in_array($updateFlagFilter, ['0', '1', '2'], true)) { $contract_cancel_flag = $request->input('contract_cancel_flag', '');
$updateFlagFilter = '0';
}
$contract_cancel_flag = $params['contract_cancel_flag'] ?? '';
// ===== クエリ(結合込み)===== // ===== クエリ(結合込み)=====
$q = DB::table('regular_contract as rc') $q = DB::table('regular_contract as rc')
->leftJoin('user as u', 'u.user_id', '=', 'rc.user_id') ->leftJoin('user as u', 'u.user_id', '=', 'rc.user_id')
->leftJoin('usertype as t', 't.user_categoryid', '=', 'rc.user_categoryid') ->leftJoin('usertype as t', 't.user_categoryid', '=', 'rc.user_categoryid')
->leftJoin('park as p', 'p.park_id', '=', 'rc.park_id') ->leftJoin('park as p', 'p.park_id', '=', 'rc.park_id')
->leftJoin('zone as z', 'z.zone_id', '=', 'rc.zone_id')
->select([ ->select([
'rc.*', // rc
'u.user_seq', 'rc.contract_id',
'rc.contract_qr_id',
'rc.user_id',
'rc.user_categoryid',
'rc.reserve_id',
'rc.park_id',
'rc.price_parkplaceid',
'rc.user_securitynum',
'rc.reserve_date',
'rc.contract_reserve',
'rc.contract_created_at',
'rc.contract_updated_at',
'rc.contract_cancelday',
'rc.contract_flag',
'rc.contract_permission',
'rc.contract_cancel_flag',
'rc.tag_qr_flag',
'rc.update_flag',
'rc.park_position',
'rc.ope_id',
// user
'u.user_name', 'u.user_name',
'u.user_phonetic', 'u.user_phonetic',
'u.user_mobile', 'u.user_mobile',
'u.user_homephone', 'u.user_homephone',
'u.user_primemail', 'u.user_primemail',
'u.user_regident_zip', // usertype & park
'u.user_tag_serial',
DB::raw('t.print_name as usertype_name'), DB::raw('t.print_name as usertype_name'),
't.usertype_subject1',
't.usertype_subject2',
't.usertype_subject3',
DB::raw('p.park_name as park_name'), DB::raw('p.park_name as park_name'),
DB::raw('z.zone_name as zone_name'),
]); ]);
// ===== LIKE / キーワード ===== // ===== LIKE / キーワード =====
@ -187,13 +93,8 @@ class RegularContractController
if ($user_id !== '') { if ($user_id !== '') {
$q->where('rc.user_id', 'like', "%{$user_id}%"); $q->where('rc.user_id', 'like', "%{$user_id}%");
} }
if ($user_tag_serial !== '') {
$q->where('u.user_tag_serial', 'like', "%{$user_tag_serial}%");
}
if ($park_id !== '') { if ($park_id !== '') {
$q->where('rc.park_id', (int) $park_id); $q->where('rc.park_id', 'like', "%{$park_id}%");
} elseif ($selectedParkId !== '') {
$q->where('rc.park_id', (int) $selectedParkId);
} }
if ($user_phonetic !== '') { if ($user_phonetic !== '') {
$q->where('u.user_phonetic', 'like', "%{$user_phonetic}%"); $q->where('u.user_phonetic', 'like', "%{$user_phonetic}%");
@ -201,73 +102,46 @@ class RegularContractController
if ($email !== '') { if ($email !== '') {
$q->where('u.user_primemail', 'like', "%{$email}%"); $q->where('u.user_primemail', 'like', "%{$email}%");
} }
if ($user_categoryid !== '') { if ($usertype_name_kw !== '') {
$q->where('rc.user_categoryid', (int) $user_categoryid); $q->where('t.print_name', 'like', "%{$usertype_name_kw}%");
} }
if ($park_name_kw !== '') { if ($park_name_kw !== '') {
$q->where('p.park_name', 'like', "%{$park_name_kw}%"); $q->where('p.park_name', 'like', "%{$park_name_kw}%");
} }
if ($zone_name !== '') {
$q->where('z.zone_name', 'like', "%{$zone_name}%");
}
if ($merge_phonetic_normalized !== null) {
$likeKeyword = '%' . $merge_phonetic_normalized . '%';
$q->whereRaw("REPLACE(REPLACE(IFNULL(rc.chk_user_phonetic, ''), ' ', ''), ' ', '') LIKE ?", [$likeKeyword]);
}
if ($phone !== '') { if ($phone !== '') {
$q->where(function ($w) use ($phone) { $q->where(function ($w) use ($phone) {
$w->where('u.user_mobile', 'like', "%{$phone}%") $w->where('u.user_mobile', 'like', "%{$phone}%")
->orWhere('u.user_homephone', 'like', "%{$phone}%"); ->orWhere('u.user_homephone', 'like', "%{$phone}%");
}); });
} }
if ($reserve_from !== '' && ($normalized = $this->normalizeDateTimeInput($reserve_from))) {
$q->where('rc.reserve_date', '>=', $normalized); // ===== 日付範囲 =====
if ($reserve_from) {
$q->whereDate('rc.reserve_date', '>=', $reserve_from);
} }
if ($reserve_to !== '' && ($normalized = $this->normalizeDateTimeInput($reserve_to, true))) { if ($reserve_to) {
$q->where('rc.reserve_date', '<=', $normalized); $q->whereDate('rc.reserve_date', '<=', $reserve_to);
} }
if ($receipt_delivery_from !== '' && ($normalized = $this->normalizeDateTimeInput($receipt_delivery_from))) { if ($created_from) {
$q->where('rc.contract_payment_day', '>=', $normalized); $q->whereDate('rc.contract_created_at', '>=', $created_from);
} }
if ($receipt_delivery_to !== '' && ($normalized = $this->normalizeDateTimeInput($receipt_delivery_to, true))) { if ($created_to) {
$q->where('rc.contract_payment_day', '<=', $normalized); $q->whereDate('rc.contract_created_at', '<=', $created_to);
} }
if ($zone_keyword !== '') { if ($updated_from) {
$q->where(function ($w) use ($zone_keyword) { $q->whereDate('rc.contract_updated_at', '>=', $updated_from);
$w->where('rc.zone_id', 'like', "%{$zone_keyword}%")
->orWhere('rc.pplace_no', 'like', "%{$zone_keyword}%")
->orWhere('rc.old_contract_id', 'like', "%{$zone_keyword}%");
});
} }
if ($workRecordFilter === '1') { if ($updated_to) {
$q->where(function ($w) { $q->whereDate('rc.contract_updated_at', '<=', $updated_to);
$w->whereNull('rc.contract_flag')
->orWhere('rc.contract_flag', '=', 0);
});
} elseif ($workRecordFilter === '2') {
$q->where('rc.contract_flag', '=', 1);
} }
if ($created_from !== '' && ($normalized = $this->normalizeDateTimeInput($created_from))) { if ($canceled_from) {
$q->where('rc.contract_created_at', '>=', $normalized); $q->whereDate('rc.contract_cancelday', '>=', $canceled_from);
} }
if ($created_to !== '' && ($normalized = $this->normalizeDateTimeInput($created_to, true))) { if ($canceled_to) {
$q->where('rc.contract_created_at', '<=', $normalized); $q->whereDate('rc.contract_cancelday', '<=', $canceled_to);
}
if ($updated_from !== '' && ($normalized = $this->normalizeDateTimeInput($updated_from))) {
$q->where('rc.contract_updated_at', '>=', $normalized);
}
if ($updated_to !== '' && ($normalized = $this->normalizeDateTimeInput($updated_to, true))) {
$q->where('rc.contract_updated_at', '<=', $normalized);
}
if ($canceled_from !== '' && ($normalized = $this->normalizeDateTimeInput($canceled_from))) {
$q->where('rc.contract_cancelday', '>=', $normalized);
}
if ($canceled_to !== '' && ($normalized = $this->normalizeDateTimeInput($canceled_to, true))) {
$q->where('rc.contract_cancelday', '<=', $normalized);
}
if ($contract_valid_months !== '') {
$q->where('rc.enable_months', (int) $contract_valid_months);
} }
// ===== 列挙フィルタ =====
if ($contract_flag !== '') { if ($contract_flag !== '') {
$q->where('rc.contract_flag', (int) $contract_flag); $q->where('rc.contract_flag', (int) $contract_flag);
} }
@ -277,13 +151,8 @@ class RegularContractController
if ($tag_qr_flag !== '') { if ($tag_qr_flag !== '') {
$q->where('rc.tag_qr_flag', (int) $tag_qr_flag); $q->where('rc.tag_qr_flag', (int) $tag_qr_flag);
} }
if ($updateFlagFilter === '1') { if ($update_flag !== '') {
$q->where('rc.update_flag', '=', 1); $q->where('rc.update_flag', (int) $update_flag);
} elseif ($updateFlagFilter === '2') {
$q->where(function ($w) {
$w->whereNull('rc.update_flag')
->orWhere('rc.update_flag', '!=', 1);
});
} }
if ($contract_cancel_flag !== '') { if ($contract_cancel_flag !== '') {
$q->where('rc.contract_cancel_flag', (int) $contract_cancel_flag); $q->where('rc.contract_cancel_flag', (int) $contract_cancel_flag);
@ -291,16 +160,34 @@ class RegularContractController
// ===== ソート(仮想列は結合側にマッピング)===== // ===== ソート(仮想列は結合側にマッピング)=====
$sortable = [ $sortable = [
'contract_id', 'contract_qr_id', 'old_contract_id', 'zone_id', 'zone_name', 'pplace_no', 'contract_id',
'contract_periods', 'contract_periode', 'user_id', 'user_categoryid', 'reserve_id', 'park_id', 'contract_qr_id',
'price_parkplaceid', 'user_securitynum', 'reserve_date', 'contract_reserve', 'contract_created_at', 'user_id',
'contract_updated_at', 'contract_cancelday', 'contract_reduction', 'enable_months', 'printable_date', 'user_categoryid',
'billing_amount', 'contract_payment_day', 'contract_money', 'refunds', 'contract_flag', 'reserve_id',
'contract_permission', 'contract_cancel_flag', 'tag_qr_flag', 'update_flag', 'pplace_allocation_flag', 'park_id',
'settlement_transaction_id', 'contract_seal_issue', 'storage_company_code', 'share_storage_company_code', 'price_parkplaceid',
'ope_id', 'park_position', 'contract_manual', 'contract_notice', 'contract_payment_number', 'user_securitynum',
'user_name', 'user_phonetic', 'user_mobile', 'user_homephone', 'user_primemail', 'user_regident_zip', 'reserve_date',
'usertype_name', 'park_name', 'contract_reserve',
'contract_created_at',
'contract_updated_at',
'contract_cancelday',
'contract_flag',
'contract_permission',
'contract_cancel_flag',
'tag_qr_flag',
'update_flag',
'park_position',
'ope_id',
// 結合先の見出し列
'user_name',
'user_phonetic',
'user_mobile',
'user_homephone',
'user_primemail',
'usertype_name',
'park_name',
]; ];
if (!in_array($sort, $sortable, true)) { if (!in_array($sort, $sortable, true)) {
$sort = 'contract_id'; $sort = 'contract_id';
@ -311,14 +198,12 @@ class RegularContractController
'user_mobile' => 'u.user_mobile', 'user_mobile' => 'u.user_mobile',
'user_homephone' => 'u.user_homephone', 'user_homephone' => 'u.user_homephone',
'user_primemail' => 'u.user_primemail', 'user_primemail' => 'u.user_primemail',
'user_regident_zip' => 'u.user_regident_zip',
'usertype_name' => 't.print_name', 'usertype_name' => 't.print_name',
'park_name' => 'p.park_name', 'park_name' => 'p.park_name',
'zone_name' => 'z.zone_name',
]; ];
$sortColumn = $sortMap[$sort] ?? ('rc.' . $sort); $sortColumn = $sortMap[$sort] ?? ('rc.' . $sort);
$list = $q->orderBy($sortColumn, $sortType)->paginate(50)->withQueryString(); $list = $q->orderBy($sortColumn, $sortType)->paginate(50);
// ===== 画面へBlade 側が参照するすべての変数を渡す)===== // ===== 画面へBlade 側が参照するすべての変数を渡す)=====
return view('admin.regularcontracts.list', [ return view('admin.regularcontracts.list', [
@ -329,18 +214,12 @@ class RegularContractController
// 入力保持(テキスト) // 入力保持(テキスト)
'contract_qr_id' => $contract_qr_id, 'contract_qr_id' => $contract_qr_id,
'user_id' => $user_id, 'user_id' => $user_id,
'user_tag_serial' => $user_tag_serial, 'park_id' => $park_id,
'park_id' => $selectedParkId !== '' ? $selectedParkId : $park_id,
'user_phonetic' => $user_phonetic, 'user_phonetic' => $user_phonetic,
'phone' => $phone, 'phone' => $phone,
'email' => $email, 'email' => $email,
'user_categoryid' => $user_categoryid, 'usertype_name' => $usertype_name_kw,
'park_name' => $park_name_kw, 'park_name' => $park_name_kw,
'zone_keyword' => $zone_keyword,
'zone_name' => $zone_name,
'merge_phonetic' => $merge_phonetic,
'has_address' => $has_address,
'work_record' => $workRecordFilter,
// 入力保持(日付) // 入力保持(日付)
'reserve_date_from' => $reserve_from, 'reserve_date_from' => $reserve_from,
@ -351,19 +230,13 @@ class RegularContractController
'contract_updated_to' => $updated_to, 'contract_updated_to' => $updated_to,
'contract_canceled_from' => $canceled_from, 'contract_canceled_from' => $canceled_from,
'contract_canceled_to' => $canceled_to, 'contract_canceled_to' => $canceled_to,
'receipt_delivery_from' => $receipt_delivery_from,
'receipt_delivery_to' => $receipt_delivery_to,
'contract_valid_months' => $contract_valid_months,
// 入力保持(列挙) // 入力保持(列挙)
'contract_flag' => $contract_flag, 'contract_flag' => $contract_flag,
'contract_permission' => $contract_permission, 'contract_permission' => $contract_permission,
'tag_qr_flag' => $tag_qr_flag, 'tag_qr_flag' => $tag_qr_flag,
'update_flag' => $updateFlagFilter, 'update_flag' => $update_flag,
'contract_cancel_flag' => $contract_cancel_flag, 'contract_cancel_flag' => $contract_cancel_flag,
'userTypeOptions' => $this->buildUsertypeOptions(),
'parkOptions' => $this->buildParkOptions(),
]); ]);
} }
@ -445,23 +318,15 @@ class RegularContractController
*/ */
public function delete(Request $request) public function delete(Request $request)
{ {
$ids = $request->input('ids', []); $id = (int) $request->input('id');
if (!is_array($ids)) { DB::table('regular_contract')->where('contract_id', $id)->delete();
$ids = [$ids];
}
$ids = array_values(array_filter(
array_map('intval', $ids),
static fn (int $v) => $v > 0
));
if (empty($ids)) { // 例:論理削除運用にする場合(必要なら運用側で切替)
return redirect()->route('regularcontracts') // DB::table('regular_contract')->where('contract_id', $id)->update([
->with('error', '削除する定期契約が選択されていません。'); // 'contract_cancel_flag' => 1,
} // 'contract_cancelday' => now(),
// 'updated_at' => now(),
DB::table('regular_contract') // ]);
->whereIn('contract_id', $ids)
->delete();
return redirect()->route('regularcontracts')->with('success', '定期契約を削除しました。'); return redirect()->route('regularcontracts')->with('success', '定期契約を削除しました。');
} }
@ -501,115 +366,70 @@ class RegularContractController
public function export(Request $request) public function export(Request $request)
{ {
$params = $request->all(); // ── 出力タイプ(通常 / SMBC / 役所) ──────────────────────────────
$sort = $params['sort'] ?? 'contract_id'; $type = $request->query('type'); // null | smbc | city
$sortType = strtolower($params['sort_type'] ?? 'asc') === 'desc' ? 'desc' : 'asc';
$type = (string) ($request->query('type') ?? '');
$fileName = match ($type) { // ── 出力ファイル名 ───────────────────────────────────────────────
'smbc' => '定期契約マスタ_SMBC.csv', $downloadName = '定期契約マスタ.csv';
'city' => '定期契約マスタ_役所提出用.csv', if ($type === 'smbc')
default => '定期契約マスタ.csv', $downloadName = '定期契約マスタ_SMBC.csv';
}; if ($type === 'city')
$downloadName = '定期契約マスタ_役所提出用.csv';
$query = $this->buildListQuery($params); // ── 生成先storage/app/tmp 配下の一時ファイル) ─────────────────
$tmpDir = storage_path('app/tmp');
$columns = [ if (!is_dir($tmpDir)) {
'contract_id' => '契約ID', @mkdir($tmpDir, 0755, true);
'contract_qr_id' => '定期契約ID',
'old_contract_id' => '旧定期契約番号',
'pplace_no' => '車室番号',
'user_id' => '利用者ID',
'user_categoryid' => '利用者分類ID',
'tag_qr_flag' => 'タグ・QR',
'park_id' => '駐輪場ID',
'reserve_date' => '予約日時',
'contract_periods' => '有効期間S',
'contract_periode' => '有効期間E',
'price_parkplaceid' => '駐輪場所ID',
'user_securitynum' => '防犯登録番号',
'contract_created_at' => '契約日時',
'contract_updated_at' => '更新可能日',
'contract_cancelday' => '解約日時',
'contract_reduction' => '減免措置',
'enable_months' => '定期有効月数',
'printable_date' => 'シール印刷可能日',
'billing_amount' => '請求金額',
'pplace_allocation_flag' => '車室割り当てフラグ',
'contract_payment_day' => '授受日時',
'contract_money' => '授受金額',
'contract_flag' => '授受フラグ',
'settlement_transaction_id' => '決済トランザクションID',
'contract_seal_issue' => 'シール発行数',
'storage_company_code' => '収納企業コード',
'share_storage_company_code' => '共有先収納企業コード',
'accept_number' => '受付番号',
'update_flag' => '(更新元)契約更新済フラグ',
'vehicle_type_id' => '車種区分ID',
'chk_user_phonetic' => 'チェック用_フリガナ',
'user_regident_zip' => 'チェック用_居住所郵便番号',
'user_mobile' => 'チェック用_携帯電話番号',
'user_homephone' => 'チェック用_自宅電話番号',
'old_member_number' => 'チェック用_旧会員番号',
'user_name' => '利用者氏名',
'user_phonetic' => '利用者フリガナ',
'park_name' => '駐輪場名',
'zone_name' => 'ゾーン名',
'usertype_name' => '利用者分類名',
];
$dateColumns = [
'contract_periods',
'contract_periode',
'contract_created_at',
'contract_updated_at',
'contract_cancelday',
'printable_date',
'contract_payment_day',
'reserve_date',
];
$rows = $query->orderBy($sort, $sortType)->get();
$headers = [
'Content-Type' => 'text/csv; charset=Shift_JIS',
'Content-Disposition' => "attachment; filename=\"{$fileName}\"",
];
return response()->streamDownload(function () use ($rows, $columns, $dateColumns) {
$handle = fopen('php://output', 'w');
$headerRow = array_map(fn ($label) => mb_convert_encoding($label, 'SJIS-win', 'UTF-8'), array_values($columns));
fputcsv($handle, $headerRow);
foreach ($rows as $row) {
$line = [];
foreach ($columns as $key => $label) {
$value = $row->{$key} ?? '';
if (in_array($key, $dateColumns, true) && $value) {
try {
$value = \Illuminate\Support\Carbon::parse($value)->format(str_contains($key, '_day') || str_contains($key, '_date') ? 'Y-m-d H:i' : 'Y-m-d');
} catch (\Throwable $e) {
$value = (string) $value;
}
} elseif ($key === 'tag_qr_flag' && $value !== '') {
$value = ((int) $value) === 1 ? 'QR' : 'タグ';
} elseif ($key === 'pplace_allocation_flag' && $value !== '') {
$value = ((int) $value) === 1 ? '割当済' : '未割当';
} elseif ($key === 'contract_flag' && $value !== '') {
$value = ((int) $value) === 1 ? '済' : '未';
} elseif ($key === 'update_flag' && $value !== '') {
$value = ((int) $value) === 1 ? '更新済' : '未更新';
} }
$tmpPath = $tmpDir . '/' . uniqid('regularcontracts_', true) . '.csv';
$line[] = mb_convert_encoding((string) $value, 'SJIS-win', 'UTF-8'); // ── CSV を作成Excel を考慮し UTF-8 BOM を付与) ───────────────
} $fp = fopen($tmpPath, 'w+');
fputcsv($handle, $line); if ($fp === false) {
abort(500, 'CSV一時ファイルを作成できませんでした。');
} }
// Excel 対策BOM
fwrite($fp, "\xEF\xBB\xBF");
fclose($handle); // ヘッダー行(必要に応じて列を増減)
}, $fileName, $headers); fputcsv($fp, ['定期契約ID', '利用者ID', '駐輪場ID', '契約日時']);
// ── データ取得(大量件数に備え chunk で分割取得) ────────────────
// ※ list() と同等の JOIN/SELECT を最低限に簡略化
DB::table('regular_contract as rc')
->leftJoin('user as u', 'u.user_id', '=', 'rc.user_id')
->orderBy('rc.contract_id', 'asc')
->select([
'rc.contract_qr_id',
'rc.user_id',
'rc.park_id',
'rc.contract_created_at',
])
->chunk(1000, function ($rows) use ($fp) {
foreach ($rows as $r) {
fputcsv($fp, [
$r->contract_qr_id,
$r->user_id,
$r->park_id,
$r->contract_created_at,
]);
}
});
fclose($fp);
// ── ダウンロードレスポンス(送信後に一時ファイル削除) ────────────
return response()->download(
$tmpPath,
$downloadName,
[
'Content-Type' => 'text/csv; charset=UTF-8',
'Content-Disposition' => 'attachment; filename="' . $downloadName . '"',
'Pragma' => 'no-cache',
'Cache-Control' => 'must-revalidate, post-check=0, pre-check=0',
'Expires' => '0',
]
)->deleteFileAfterSend(true);
} }
@ -703,178 +523,6 @@ class RegularContractController
->with('success', '定期契約を登録しました。'); ->with('success', '定期契約を登録しました。');
} }
private function buildListQuery(array $params)
{
$contract_qr_id = trim((string)($params['contract_qr_id'] ?? ''));
$user_id = trim((string)($params['user_id'] ?? ''));
$user_tag_serial = trim((string)($params['user_tag_serial'] ?? ''));
$park_id = trim((string)($params['park_id'] ?? ''));
$selectedParkId = trim((string)($params['selected_park_id'] ?? ''));
$user_phonetic = trim((string)($params['user_phonetic'] ?? ''));
$phone = trim((string)($params['phone'] ?? ''));
$email = trim((string)($params['email'] ?? ''));
$user_categoryid = trim((string)($params['user_categoryid'] ?? ''));
$park_name_kw = trim((string)($params['park_name'] ?? ''));
$zone_keyword = trim((string)($params['zone_keyword'] ?? ''));
$zone_name = trim((string)($params['zone_name'] ?? ''));
$merge_phonetic_input = $params['merge_phonetic'] ?? '';
$merge_phonetic = trim((string)$merge_phonetic_input);
$merge_phonetic_normalized = $this->normalizePhoneticKeyword($merge_phonetic);
$workRecordFilter = (string)($params['work_record'] ?? '0');
if (!in_array($workRecordFilter, ['0', '1', '2'], true)) {
$workRecordFilter = '0';
}
$reserve_from = $params['reserve_date_from'] ?? '';
$reserve_to = $params['reserve_date_to'] ?? '';
$created_from = $params['contract_created_from'] ?? '';
$created_to = $params['contract_created_to'] ?? '';
$updated_from = $params['contract_updated_from'] ?? '';
$updated_to = $params['contract_updated_to'] ?? '';
$canceled_from = $params['contract_canceled_from'] ?? '';
$canceled_to = $params['contract_canceled_to'] ?? '';
$receipt_delivery_from = $params['receipt_delivery_from'] ?? '';
$receipt_delivery_to = $params['receipt_delivery_to'] ?? '';
$contract_valid_months = $params['contract_valid_months'] ?? '';
$contract_flag = $params['contract_flag'] ?? '';
$contract_permission = $params['contract_permission'] ?? '';
$tag_qr_flag = $params['tag_qr_flag'] ?? '';
$updateFlagFilter = (string)($params['update_flag'] ?? '0');
if (!in_array($updateFlagFilter, ['0', '1', '2'], true)) {
$updateFlagFilter = '0';
}
$contract_cancel_flag = $params['contract_cancel_flag'] ?? '';
$q = DB::table('regular_contract as rc')
->leftJoin('user as u', 'u.user_id', '=', 'rc.user_id')
->leftJoin('usertype as t', 't.user_categoryid', '=', 'rc.user_categoryid')
->leftJoin('park as p', 'p.park_id', '=', 'rc.park_id')
->leftJoin('zone as z', 'z.zone_id', '=', 'rc.zone_id')
->select([
'rc.*',
'u.user_seq',
'u.user_name',
'u.user_phonetic',
'u.user_mobile',
'u.user_homephone',
'u.user_primemail',
'u.user_regident_zip',
'u.user_tag_serial',
DB::raw('t.print_name as usertype_name'),
't.usertype_subject1',
't.usertype_subject2',
't.usertype_subject3',
DB::raw('p.park_name as park_name'),
DB::raw('z.zone_name as zone_name'),
]);
if ($contract_qr_id !== '') {
$q->where('rc.contract_qr_id', 'like', "%{$contract_qr_id}%");
}
if ($user_id !== '') {
$q->where('rc.user_id', 'like', "%{$user_id}%");
}
if ($user_tag_serial !== '') {
$q->where('u.user_tag_serial', 'like', "%{$user_tag_serial}%");
}
if ($park_id !== '') {
$q->where('rc.park_id', (int)$park_id);
} elseif ($selectedParkId !== '') {
$q->where('rc.park_id', (int)$selectedParkId);
}
if ($user_phonetic !== '') {
$q->where('u.user_phonetic', 'like', "%{$user_phonetic}%");
}
if ($email !== '') {
$q->where('u.user_primemail', 'like', "%{$email}%");
}
if ($user_categoryid !== '') {
$q->where('rc.user_categoryid', (int)$user_categoryid);
}
if ($park_name_kw !== '') {
$q->where('p.park_name', 'like', "%{$park_name_kw}%");
}
if ($zone_name !== '') {
$q->where('z.zone_name', 'like', "%{$zone_name}%");
}
if ($merge_phonetic_normalized !== null) {
$likeKeyword = '%' . $merge_phonetic_normalized . '%';
$q->whereRaw("REPLACE(REPLACE(IFNULL(rc.chk_user_phonetic, ''), ' ', ''), ' ', '') LIKE ?", [$likeKeyword]);
}
if ($phone !== '') {
$q->where(function ($w) use ($phone) {
$w->where('u.user_mobile', 'like', "%{$phone}%")
->orWhere('u.user_homephone', 'like', "%{$phone}%");
});
}
if ($reserve_from !== '' && ($normalized = $this->normalizeDateTimeInput($reserve_from))) {
$q->where('rc.reserve_date', '>=', $normalized);
}
if ($reserve_to !== '' && ($normalized = $this->normalizeDateTimeInput($reserve_to, true))) {
$q->where('rc.reserve_date', '<=', $normalized);
}
if ($receipt_delivery_from !== '' && ($normalized = $this->normalizeDateTimeInput($receipt_delivery_from))) {
$q->where('rc.contract_payment_day', '>=', $normalized);
}
if ($receipt_delivery_to !== '' && ($normalized = $this->normalizeDateTimeInput($receipt_delivery_to, true))) {
$q->where('rc.contract_payment_day', '<=', $normalized);
}
if ($zone_keyword !== '') {
$q->where(function ($w) use ($zone_keyword) {
$w->where('rc.zone_id', 'like', "%{$zone_keyword}%")
->orWhere('rc.pplace_no', 'like', "%{$zone_keyword}%")
->orWhere('rc.old_contract_id', 'like', "%{$zone_keyword}%");
});
}
if ($workRecordFilter === '1') {
$q->where(function ($w) {
$w->whereNull('rc.contract_flag')
->orWhere('rc.contract_flag', '=', 0);
});
} elseif ($workRecordFilter === '2') {
$q->where('rc.contract_flag', '=', 1);
}
if ($created_from !== '' && ($normalized = $this->normalizeDateTimeInput($created_from))) {
$q->where('rc.contract_created_at', '>=', $normalized);
}
if ($created_to !== '' && ($normalized = $this->normalizeDateTimeInput($created_to, true))) {
$q->where('rc.contract_created_at', '<=', $normalized);
}
if ($updated_from !== '' && ($normalized = $this->normalizeDateTimeInput($updated_from))) {
$q->where('rc.contract_updated_at', '>=', $normalized);
}
if ($updated_to !== '' && ($normalized = $this->normalizeDateTimeInput($updated_to, true))) {
$q->where('rc.contract_updated_at', '<=', $normalized);
}
if ($canceled_from !== '' && ($normalized = $this->normalizeDateTimeInput($canceled_from))) {
$q->where('rc.contract_cancelday', '>=', $normalized);
}
if ($canceled_to !== '' && ($normalized = $this->normalizeDateTimeInput($canceled_to, true))) {
$q->where('rc.contract_cancelday', '<=', $normalized);
}
if ($contract_valid_months !== '') {
$q->where('rc.enable_months', (int)$contract_valid_months);
}
if ($contract_flag !== '') {
$q->where('rc.contract_flag', (int)$contract_flag);
}
if ($contract_permission !== '') {
$q->where('rc.contract_permission', (int)$contract_permission);
}
if ($tag_qr_flag !== '') {
$q->where('rc.tag_qr_flag', (int)$tag_qr_flag);
}
if ($updateFlagFilter === '1') {
$q->where('rc.update_flag', '=', 1);
} elseif ($updateFlagFilter === '2') {
$q->where(function ($w) {
$w->whereNull('rc.update_flag')
->orWhere('rc.update_flag', '!=', 1);
});
}
if ($contract_cancel_flag !== '') {
$q->where('rc.contract_cancel_flag', (int)$contract_cancel_flag);
}
return $q;
}
} }

View File

@ -15,166 +15,119 @@ use Response;
class RegularTypeController extends Controller class RegularTypeController extends Controller
{ {
public function list(Request $request)
{
$inputs = [
'isMethodPost' => 0,
'isExport' => 0,
'sort' => $request->input('sort', ''),
'sort_type' => $request->input('sort_type', ''),
'page' => $request->get('page', 1),
];
$inputs['isMethodPost'] = $request->isMethod('post');
$inputs['list'] = RegularType::search($inputs);
if ($inputs['list']->total() > 0 && $inputs['page'] > $inputs['list']->lastPage()) {
return redirect()->route('regular_types');
}
return view('admin.regular_types.list', $inputs);
}
public function add(Request $request) public function add(Request $request)
{ {
// 画面用データ
$dataList = $this->getDataDropList();
// フォーム入力(画面初期表示用)
$inputs = [ $inputs = [
'city_id' => $request->input('city_id'), 'regular_type_id' => $request->input('regular_type_id'), // 定期種別ID
'regular_class_1' => $request->input('regular_class_1'), 'city_id' => $request->input('city_name', ''), // 市区名
'regular_class_2' => $request->input('regular_class_2'), 'regular_class_1' => $request->input('regular_class_1'), // 定期種別1
'regular_class_3' => $request->input('regular_class_3'), 'regular_class_2' => $request->input('regular_class_2'), // 定期種別2
'regular_class_6' => $request->input('regular_class_6'), 'regular_class_3' => $request->input('regular_class_3'), // 定期種別3
'regular_class_12' => $request->input('regular_class_12'), 'regular_class_6' => $request->input('regular_class_6'), // 定期種別6
'memo' => $request->input('memo'), 'regular_class_12' => $request->input('regular_class_12'), // 定期種別12
'memo' => $request->input('memo'), // 備考
]; ];
$viewData = array_merge($inputs, $dataList); $dataList = $this->getDataDropList();
$inputs = array_merge($inputs, $dataList);
$record = new RegularType();
if ($request->isMethod('POST')) { if ($request->isMethod('POST')) {
$type = false;
$validation = new RegularTypeRequest(); $validation = new RegularTypeRequest();
$rules = $validation->rules(); $rules = $validation->rules();
$validator = Validator::make($request->all(), $rules, $validation->messages()); $validator = Validator::make($request->all(), $rules, $validation->messages());
if (!$validator->fails()) {
if ($validator->fails()) { \DB::transaction(function () use ($inputs, &$type) {
return redirect()
->back()
->withErrors($validator)
->withInput();
}
// バリデーション成功
$payload = array_intersect_key($request->all(), array_flip([
'city_id',
'regular_class_1',
'regular_class_2',
'regular_class_3',
'regular_class_6',
'regular_class_12',
'memo',
]));
DB::transaction(function () use ($payload) {
$new = new RegularType(); $new = new RegularType();
$new->fill($payload); $new->fill($inputs);
$new->operator_id = \Auth::user()->ope_id; if ($new->save()) {
$new->save(); $type = true;
}
}); });
if ($type) {
$request->session()->flash('success', __('登録しました。')); $request->session()->flash('success', __('新しい成功を創造する。'));
return redirect()->route('regular_types'); return redirect()->route('regular_types');
} else {
$request->session()->flash('error', __('新しい作成に失敗しました'));
}
} else {
$inputs['errorMsg'] = $this->__buildErrorMessasges($validator);
}
} }
return view('admin.regular_types.add', array_merge($viewData, [ return view('admin.regular_types.add', $inputs);
'record' => $record,
]));
} }
public function edit(Request $request, $id, $view = '') public function edit(Request $request, $pk, $view = '')
{ {
// --- データ取得 --- $regular_type = RegularType::getByPk($pk);
$record = RegularType::getById($id); if (empty($pk) || empty($regular_type)) {
if (empty($id) || empty($record)) { abort('404');
abort(404);
} }
$data = $regular_type->getAttributes();
// --- 初期表示用データ --- $dataList = $this->getDataDropList();
$data = array_merge( $data = array_merge($data, $dataList);
$record->getAttributes(),
$this->getDataDropList(),
[
'record' => $record,
'isEdit' => true,
]
);
// --- 更新処理 ---
if ($request->isMethod('POST')) { if ($request->isMethod('POST')) {
$type = false;
$validation = new RegularTypeRequest(); $validation = new RegularTypeRequest();
$rules = $validation->rules(); $rules = $validation->rules();
$validator = Validator::make($request->all(), $rules, $validation->messages()); $validator = Validator::make($request->all(), $rules, $validation->messages());
// city_name → city_id の補正
$requestAll = $request->all(); $requestAll = $request->all();
if (isset($requestAll['city_name']) && !isset($requestAll['city_id'])) { $requestAll['city_id'] = $request->input('city_name');
$requestAll['city_id'] = $requestAll['city_name']; $data = array_merge($data, $requestAll);
} if (!$validator->fails()) {
// 書き込み対象のカラムのみ許可 \DB::transaction(function () use ($data, &$type, $regular_type) {
$payload = array_intersect_key($requestAll, array_flip([ $regular_type->fill($data);
'city_id', $regular_type->save();
'regular_class_1', $type = true;
'regular_class_2',
'regular_class_3',
'regular_class_6',
'regular_class_12',
'memo',
]));
// バリデーションエラー
if ($validator->fails()) {
$data['errorMsg'] = $this->buildErrorMessages($validator);
$data = array_merge($data, $payload);
if ($view !== '') return view($view, $data);
return view('admin.regular_types.edit', $data);
}
// 更新
DB::transaction(function () use (&$record, $payload) {
$record->fill($payload);
$record->save();
}); });
if ($type) {
$request->session()->flash('success', __('更新しました')); $request->session()->flash('success', __('更新に成功しました'));
return redirect()->route('regular_types'); return redirect()->route('regular_types');
} else {
$request->session()->flash('error', __('更新に失敗しました'));
} }
} else {
// --- 画面表示 --- $data['errorMsg'] = $this->__buildErrorMessasges($validator);
if ($view !== '') { }
}
if ($view != '') {
return view($view, $data); return view($view, $data);
} }
return view('admin.regular_types.edit', $data); return view('admin.regular_types.edit', $data);
} }
/** バリデーションエラーをまとめる */ public function delete(Request $request)
protected function buildErrorMessages(\Illuminate\Contracts\Validation\Validator $validator): string
{ {
return implode("\n", $validator->errors()->all()); $arr_pk = $request->get('pk');
if ($arr_pk) {
if (RegularType::deleteByPk($arr_pk)) {
return redirect()->route('regular_types')->with('success', __("削除が完了しました。"));
} else {
return redirect()->route('regular_types')->with('error', __('削除に失敗しました。'));
} }
public function delete(Request $request, $id = null)
{
// 一覧画面checkbox で複数削除)
$ids = $request->input('pk');
// 編集画面(単体削除)
if ($id) {
$ids = [$id];
} }
return redirect()->route('regular_types')->with('error', __('削除するユーザーを選択してください。'));
// 削除対象が空の場合
if (empty($ids)) {
return redirect()
->route('regular_types')
->with('error', '削除対象が選択されていません。');
} }
// 削除処理
RegularType::destroy($ids);
return redirect()
->route('regular_types')
->with('success', '削除しました。');
}
public function export(Request $request) public function export(Request $request)
{ {
@ -215,20 +168,5 @@ class RegularTypeController extends Controller
$data['cities'] = City::getList(); $data['cities'] = City::getList();
return $data; return $data;
} }
public function list(Request $request)
{
$sort = $request->input('sort', 'regular_type_id');
$sort_type = $request->input('sort_type', 'asc');
$list = \App\Models\RegularType::orderBy($sort, $sort_type)->paginate(20);
return view('admin.regular_types.list', [
'list' => $list,
'sort' => $sort,
'sort_type' => $sort_type,
]);
}
} }

View File

@ -9,45 +9,15 @@ use Illuminate\Support\Facades\DB;
class ReservationController extends Controller class ReservationController extends Controller
{ {
/** /**
* 一覧表示GET/POST * 予約者一覧表示
*/ */
public function list(Request $request) public function list(Request $request)
{ {
// ベースクエリを構築 $q = DB::table('reserve as r')
$q = DB::table('regular_contract as rc')
->leftJoin('user as u','rc.user_id','=','u.user_id')
->select([ ->select([
'rc.contract_id', 'r.user_id',
'rc.contract_qr_id',
'rc.user_id',
'rc.user_categoryid',
'rc.park_id',
'rc.contract_created_at',
'rc.contract_periods',
'rc.contract_periode',
'rc.tag_qr_flag',
'rc.contract_flag',
'rc.contract_cancel_flag',
'rc.contract_payment_day',
'rc.contract_money',
'rc.billing_amount',
'rc.contract_permission',
'rc.contract_manual',
'rc.contract_notice',
'rc.update_flag',
'rc.800m_flag',
'rc.price_parkplaceid',
'rc.psection_id',
'rc.reserve_date',
'p.park_name',
'u.user_name', 'u.user_name',
'u.user_phonetic', 'u.user_phonetic',
'u.user_mobile',
'u.user_seq',
'u.user_homephone',
'u.user_primemail',
'u.user_gender',
'u.user_birthdate',
'u.user_regident_zip', 'u.user_regident_zip',
'u.user_regident_pre', 'u.user_regident_pre',
'u.user_regident_city', 'u.user_regident_city',
@ -56,195 +26,74 @@ class ReservationController extends Controller
'u.user_relate_pre', 'u.user_relate_pre',
'u.user_relate_city', 'u.user_relate_city',
'u.user_relate_add', 'u.user_relate_add',
'u.user_graduate', 'u.user_birthdate',
'u.user_workplace', 'u.user_gender',
'u.user_mobile',
'u.user_homephone',
'u.user_school', 'u.user_school',
'u.user_graduate',
'u.user_remarks', 'u.user_remarks',
'u.user_tag_serial_64', 'r.reserve_id',
'u.user_reduction', 'r.park_id',
DB::raw('rc.user_securitynum as crime_prevention'), 'p.park_name',
DB::raw('rc.contract_seal_issue as seal_issue_count'), 'r.price_parkplaceid',
DB::raw("CASE rc.enable_months 'r.psection_id',
WHEN 1 THEN '月極(1ヶ月)' 'r.reserve_date',
WHEN 3 THEN '3ヶ月' 'r.reserve_reduction as reduction',
WHEN 6 THEN '6ヶ月' 'r.800m_flag as within_800m_flag',
WHEN 12 THEN '年'
ELSE CONCAT(rc.enable_months, 'ヶ月') END as ticket_type"),
DB::raw('ps.psection_subject as vehicle_type'),
// 利用者分類のラベルusertype テーブルの subject を取得)
DB::raw('ut.usertype_subject1 as user_category1'),
DB::raw('ut.usertype_subject2 as user_category2'),
DB::raw('ut.usertype_subject3 as user_category3'),
]) ])
->leftJoin('park as p', 'rc.park_id', '=', 'p.park_id') ->leftJoin('user as u', 'r.user_id', '=', 'u.user_id')
->leftJoin('psection as ps', 'rc.psection_id', '=', 'ps.psection_id') ->leftJoin('park as p', 'r.park_id', '=', 'p.park_id'); // 追加
->leftJoin('usertype as ut', 'u.user_categoryid', '=', 'ut.user_categoryid');
// ===== 絞り込み条件 ===== // フィルター条件
// 駐輪場で絞る(完全一致)
if ($request->filled('park_id')) { if ($request->filled('park_id')) {
$q->where('rc.park_id', $request->park_id); $q->where('r.park_id', $request->input('park_id'));
} }
// 利用者IDで絞る完全一致
if ($request->filled('user_id')) { if ($request->filled('user_id')) {
$q->where('rc.user_id', $request->user_id); $q->where('r.user_id', $request->input('user_id'));
} }
if ($request->filled('user_categoryid')) {
// 利用者分類で絞る(※ select の value を user_categoryid にしているため、user テーブルのカラムで比較) $q->where('r.user_categoryid', $request->input('user_categoryid'));
if ($request->filled('user_category1')) {
$q->where('u.user_categoryid', $request->user_category1);
} }
// タグシリアルで部分一致検索
if ($request->filled('user_tag_serial')) { if ($request->filled('user_tag_serial')) {
$q->where('u.user_tag_serial', 'like', '%' . $request->input('user_tag_serial') . '%'); $q->where('u.user_tag_serial', 'like', '%' . $request->input('user_tag_serial') . '%');
} }
// タグシリアル64進で部分一致検索
if ($request->filled('user_tag_serial_64')) { if ($request->filled('user_tag_serial_64')) {
$val = $request->user_tag_serial_64; $q->where('u.user_tag_serial_64', 'like', '%' . $request->input('user_tag_serial_64') . '%');
$q->where('u.user_tag_serial_64','like','%'.$val.'%');
} }
// フリガナで部分一致
if ($request->filled('user_phonetic')) { if ($request->filled('user_phonetic')) {
$q->where('u.user_phonetic', 'like', '%' . $request->user_phonetic . '%'); $q->where('u.user_phonetic', 'like', '%' . $request->input('user_phonetic') . '%');
} }
// 携帯電話で部分一致
if ($request->filled('user_mobile')) { if ($request->filled('user_mobile')) {
$q->where('u.user_mobile', 'like', '%' . $request->user_mobile . '%'); $q->where(function($sub) use ($request) {
$sub->where('u.user_mobile', 'like', '%' . $request->input('user_mobile') . '%')
->orWhere('u.user_homephone', 'like', '%' . $request->input('user_mobile') . '%');
});
} }
// メールアドレスで部分一致
if ($request->filled('user_primemail')) { if ($request->filled('user_primemail')) {
$q->where('u.user_primemail', 'like', '%' . $request->user_primemail . '%'); $q->where('u.user_primemail', 'like', '%' . $request->input('user_primemail') . '%');
} }
// 勤務先で部分一致
if ($request->filled('user_workplace')) { if ($request->filled('user_workplace')) {
$q->where('u.user_workplace', 'like', '%' . $request->user_workplace . '%'); $q->where('u.user_workplace', 'like', '%' . $request->input('user_workplace') . '%');
} }
// 学校で部分一致
if ($request->filled('user_school')) { if ($request->filled('user_school')) {
$q->where('u.user_school', 'like', '%' . $request->user_school . '%'); $q->where('u.user_school', 'like', '%' . $request->input('user_school') . '%');
} }
// ===== ソート処理 ===== // ソート
// 指定があればその列でソート、なければデフォルトで契約IDの昇順 $sort = $request->input('sort', 'r.reserve_id');
$sort = $request->input('sort'); // null 許容 $sortType = $request->input('sort_type', 'desc');
$sortType = $request->input('sort_type','asc'); $allowSorts = ['r.reserve_id', 'r.reserve_date', 'r.reserve_start', 'r.reserve_end'];
if (!in_array($sort, $allowSorts)) {
$allowSorts = [ $sort = 'r.reserve_id';
'rc.contract_id',
'rc.user_id',
'u.user_name',
'rc.tag_qr_flag',
'p.park_name',
];
if ($sort && in_array($sort, $allowSorts)) {
$sortType = $sortType === 'desc' ? 'desc' : 'asc';
$q->orderBy($sort, $sortType);
} else {
// デフォルトソート
$sort = null;
$sortType = null;
$q->orderBy('rc.contract_id','asc');
} }
$sortType = ($sortType === 'asc') ? 'asc' : 'desc';
// ページネーション(クエリ文字列を引き継ぐ) $rows = $q->orderBy($sort, $sortType)->paginate(20)->withQueryString();
$rows = $q->paginate(20)->appends($request->query());
// 駐輪場セレクト用データ取得 // 駐輪場リスト取得(必要なら)
$parks = DB::table('park')->select('park_id', 'park_name')->orderBy('park_name')->get(); $parks = DB::table('park')->select('park_id', 'park_name')->get();
// 利用者分類セレクト用:実際に使用されている分類のみを取得する return view('admin.reservation.list', compact('rows', 'sort', 'sortType', 'parks'));
$categories = $this->buildCategoryOptions(true);
// ビューに渡す
return view('admin.reservation.list', compact('rows', 'sort', 'sortType', 'parks', 'categories'));
}
/**
* 詳細表示
*/
public function info($id)
{
// 指定契約IDの詳細を取得
$contract = DB::table('regular_contract as rc')
->select([
'rc.*',
'p.park_name',
'u.user_name',
'u.user_phonetic',
'u.user_mobile',
'u.user_homephone',
'u.user_primemail',
'u.user_gender',
'u.user_birthdate',
'u.user_regident_city',
])
->leftJoin('park as p', 'rc.park_id', '=', 'p.park_id')
->leftJoin('user as u', 'rc.user_id', '=', 'u.user_id')
->where('rc.contract_id', $id)
->first();
if (!$contract) { abort(404); }
return view('admin.reservation.info', compact('contract'));
}
/**
* 利用者分類選択肢を取得
*
* @param bool $onlyUsed true の場合は regular_contract に出現する分類のみ返す
* @return array [user_categoryid => label, ...]
*/
private function buildCategoryOptions(bool $onlyUsed = false): array
{
if (! $onlyUsed) {
// 全件取得(既存の挙動)
return DB::table('usertype')
->orderBy('user_categoryid', 'asc')
->get()
->mapWithKeys(function ($row) {
$label = collect([
$row->usertype_subject1 ?? '',
$row->usertype_subject2 ?? '',
$row->usertype_subject3 ?? '',
])->filter(fn ($v) => $v !== '')->implode('/');
return [$row->user_categoryid => $label !== '' ? $label : (string) $row->user_categoryid];
})
->toArray();
}
// 実際に使用されている分類のみ取得する(内部結合で user と regular_contract と紐付くもの)
$rows = DB::table('usertype as ut')
->join('user as u', 'u.user_categoryid', '=', 'ut.user_categoryid')
->join('regular_contract as rc', 'rc.user_id', '=', 'u.user_id')
->select(
'ut.user_categoryid',
'ut.usertype_subject1',
'ut.usertype_subject2',
'ut.usertype_subject3'
)
->groupBy('ut.user_categoryid', 'ut.usertype_subject1', 'ut.usertype_subject2', 'ut.usertype_subject3')
->orderBy('ut.user_categoryid', 'asc')
->get();
// ラベルを組み立てて配列で返す
return $rows->mapWithKeys(function ($row) {
$label = collect([
$row->usertype_subject1 ?? '',
$row->usertype_subject2 ?? '',
$row->usertype_subject3 ?? '',
])->filter(fn ($v) => $v !== '')->implode('/');
return [$row->user_categoryid => $label !== '' ? $label : (string) $row->user_categoryid];
})->toArray();
} }
} }

View File

@ -1,118 +1,309 @@
<?php <?php
declare(strict_types=1);
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use Illuminate\Support\Facades\Schema;
use App\Http\Requests\ReserveRequest; use Illuminate\Http\Request;
use App\Services\ReserveService; use Illuminate\Support\Facades\DB;
use Illuminate\Http\RedirectResponse; use Illuminate\Support\Facades\Validator;
use Illuminate\View\View;
final class ReservesController extends Controller class ReservesController
{ {
public function __construct( /**
private readonly ReserveService $reserveService * 予約一覧
) { * - 基本表: reserver
* - 付加情報: useru, parkp
* - 画面変数: $list, $sort, $sort_type利用者マスタの書式に準拠
*
* reserve テーブルの主な列:
* reserve_id, contract_id, user_id, park_id, price_parkplaceid, psection_id,
* reserve_date, reserve_start, reserve_end, reserve_cancelday, valid_flag, ope_id など
* DDL dump の通り【turn12file6†L15-L37】
*/
public function list(Request $request)
{
// ── 並び順(既定: reserve_id DESC──────────────────────────────
$sort = $request->input('sort', 'reserve_id');
$sortType = strtolower($request->input('sort_type', 'desc')) === 'asc' ? 'asc' : 'desc';
// ── 絞り込み(必要最低限:利用者/駐輪場/期間)──────────────────
$userId = trim((string) $request->input('user_id', ''));
$parkId = trim((string) $request->input('park_id', ''));
$fromDt = $request->input('reserve_date_from', '');
$toDt = $request->input('reserve_date_to', '');
$keyword = trim((string) $request->input('keyword', '')); // 利用者名かな など
// ── クエリ構築 ────────────────────────────────────────────────
$q = DB::table('reserve as r')
->leftJoin('user as u', 'u.user_id', '=', 'r.user_id') // user: user_name, user_phonetic 等【turn12file9†L26-L34】
->leftJoin('park as p', 'p.park_id', '=', 'r.park_id') // park: park_name 等【turn12file4†L17-L25】
->select([
'r.reserve_id',
'r.contract_id',
'r.user_id',
'r.park_id',
'r.price_parkplaceid',
'r.psection_id',
'r.reserve_date',
'r.reserve_start',
'r.reserve_end',
'r.reserve_cancelday',
'r.valid_flag',
'r.ope_id',
DB::raw('u.user_name as user_name'),
DB::raw('u.user_phonetic as user_phonetic'),
DB::raw('u.user_mobile as user_mobile'),
DB::raw('p.park_name as park_name'),
]);
if ($userId !== '')
$q->where('r.user_id', 'like', "%{$userId}%");
if ($parkId !== '')
$q->where('r.park_id', 'like', "%{$parkId}%");
if ($fromDt)
$q->whereDate('r.reserve_date', '>=', $fromDt);
if ($toDt)
$q->whereDate('r.reserve_date', '<=', $toDt);
if ($keyword !== '') {
$q->where(function ($w) use ($keyword) {
$w->where('u.user_name', 'like', "%{$keyword}%")
->orWhere('u.user_phonetic', 'like', "%{$keyword}%");
});
} }
/** // ソート許可カラムJOIN 先も含む)
* 予約一覧(検索・並び替え・ページング) $sortable = [
* Why: Controller を薄くし、検索条件・クエリ構築は Service に集約する。 'reserve_id',
*/ 'contract_id',
public function index(ReserveRequest $request): View 'user_id',
{ 'park_id',
$result = $this->reserveService->paginate($request->payload()); 'reserve_date',
'reserve_start',
'reserve_end',
'reserve_cancelday',
'valid_flag',
'ope_id',
'user_name',
'user_phonetic',
'user_mobile',
'park_name',
];
if (!in_array($sort, $sortable, true))
$sort = 'reserve_id';
return view('admin.reserves.index', $result); $sortMap = [
} 'user_name' => 'u.user_name',
'user_phonetic' => 'u.user_phonetic',
'user_mobile' => 'u.user_mobile',
'park_name' => 'p.park_name',
];
$sortCol = $sortMap[$sort] ?? ('r.' . $sort);
/** $list = $q->orderBy($sortCol, $sortType)->paginate(50);
* 新規(画面)
* Why: プルダウン等の初期表示データ取得は Service に寄せる。
*/
public function create(): View
{
$form = $this->reserveService->getCreateForm();
return view('admin.reserves.create', [ return view('admin.reserves.list', [
'record' => null, 'list' => $list,
'sort' => $sort,
// Blade: ($userTypes ?? []) as $id => $label 형태로 사용 'sort_type' => $sortType,
'userTypes' => $form['userTypeOptions'] ?? [], // 入力保持
'user_id' => $userId,
// Blade: ($parks ?? []) as $id => $label 'park_id' => $parkId,
'parks' => $form['parkOptions'] ?? [], 'reserve_date_from' => $fromDt,
'reserve_date_to' => $toDt,
// Blade: ($prices ?? []) as $id => $label (prine_name 표시) 'keyword' => $keyword,
'prices' => $form['priceOptions'] ?? [],
// Blade: ($psections ?? []) as $id => $label
'psections' => $form['psectionOptions'] ?? [],
// Blade: ($ptypes ?? []) as $id => $label
'ptypes' => $form['ptypeOptions'] ?? [],
]); ]);
} }
/** /**
* 新規(登録) * 予約追加GET: 画面表示 / POST: 登録)
*/ */
public function store(ReserveRequest $request): RedirectResponse public function add(Request $request)
{ {
$this->reserveService->create($request->payload()); if ($request->isMethod('get')) {
return view('admin.reserves.add');
return redirect()
->route('reserves.index')
->with('success', __('登録を完了しました。'));
} }
/** // 予約の最低限バリデーション(必要に応じて追加)
* 編集(画面) $v = Validator::make($request->all(), [
*/ 'user_id' => ['required', 'integer'],
public function edit(int $reserveId): View 'park_id' => ['required', 'integer'],
{ 'reserve_date' => ['nullable', 'date'],
$form = $this->reserveService->getEditForm($reserveId); 'reserve_start' => ['nullable', 'date'],
'reserve_end' => ['nullable', 'date'],
return view('admin.reserves.edit', [ ], [], [
'record' => $form['row'], 'user_id' => '利用者ID',
'park_id' => '駐輪場ID',
'userTypes' => $form['userTypeOptions'] ?? [],
'parks' => $form['parkOptions'] ?? [],
'prices' => $form['priceOptions'] ?? [],
'psections' => $form['psectionOptions'] ?? [],
'ptypes' => $form['ptypeOptions'] ?? [],
]); ]);
if ($v->fails()) {
return back()->withErrors($v)->withInput();
} }
DB::table('reserve')->insert([
'user_id' => (int) $request->input('user_id'),
'park_id' => (int) $request->input('park_id'),
'contract_id' => $request->input('contract_id'), // 任意regular_contract と紐づける場合【turn12file7†L20-L28】
'price_parkplaceid' => $request->input('price_parkplaceid'),
'psection_id' => $request->input('psection_id'),
'reserve_date' => $request->input('reserve_date'),
'reserve_start' => $request->input('reserve_start'),
'reserve_end' => $request->input('reserve_end'),
'valid_flag' => $request->input('valid_flag'),
'ope_id' => $request->input('ope_id'),
'created_at' => now(),
'updated_at' => now(),
]);
return redirect()->route('reserves')->with('success', '予約を登録しました。');
/**
* 編集(更新)
*/
public function update(ReserveRequest $request, int $reserveId): RedirectResponse
{
$this->reserveService->update($reserveId, $request->payload());
return redirect()
->route('reserves.index')
->with('success', __('更新を完了しました。'));
} }
/** /**
* 削除(複数削除 pk[] * 予約削除
*/ */
public function destroy(ReserveRequest $request): RedirectResponse public function delete(Request $request)
{ {
$payload = $request->payload();
$deleted = $this->reserveService->deleteMany($payload['ids'] ?? []);
return redirect() $normalizeIds = function ($raw) {
->route('reserves.index')
->with( if (is_string($raw)) {
$deleted > 0 ? 'success' : 'warning', $raw = explode(',', $raw);
$deleted > 0 ? __(':count 件を削除しました。', ['count' => $deleted]) : __('削除対象がありません。') }
if (is_array($raw) && count($raw) === 1 && is_string($raw[0]) && str_contains($raw[0], ',')) {
$raw = explode(',', $raw[0]);
}
$ids = array_map('intval', (array) $raw);
$ids = array_values(array_unique(array_filter($ids, fn($v) => $v > 0)));
return $ids;
};
if ($request->isMethod('get')) {
$ids = $normalizeIds($request->input('ids', []));
$rows = [];
if ($ids) {
$rows = DB::table('reserve as r')
->leftJoin('user as u', 'u.user_id', '=', 'r.user_id')
->leftJoin('park as p', 'p.park_id', '=', 'r.park_id')
->whereIn('r.reserve_id', $ids)
->select('r.*', 'u.user_name', 'p.park_name')
->get();
}
return view('admin.reserves.delete', compact('rows', 'ids'));
}
if ($request->post('confirmed')) {
$ids = $normalizeIds($request->input('ids', []));
if ($ids) {
$deleted = DB::table('reserve')->whereIn('reserve_id', $ids)->delete();
return redirect()->route('reserves')->with(
$deleted ? 'success' : 'warning',
$deleted ? "{$deleted} 件を削除しました。" : '対象が見つかりませんでした。'
); );
} }
}
return redirect()->route('reserves')->with('warning', '削除対象がありません。');
}
public function edit(Request $request, $reserve_id)
{
$id = (int) $reserve_id;
// 取得レコード無ければ404
$row = DB::table('reserve')->where('reserve_id', $id)->first();
if (!$row) {
abort(404);
}
// POST: 更新処理※reserveテーブルに確実にある列だけ更新
if ($request->isMethod('post')) {
$v = Validator::make($request->all(), [
'user_id' => ['required', 'integer'],
'park_id' => ['required', 'integer'],
'reserve_date' => ['nullable', 'date'],
'reserve_start' => ['nullable', 'date'],
'reserve_end' => ['nullable', 'date'],
], [], [
'user_id' => '利用者ID',
'park_id' => '駐輪場ID',
]);
if ($v->fails()) {
return back()->withErrors($v)->withInput();
}
$data = [
'contract_id' => $request->input('contract_id'),
'user_id' => (int) $request->input('user_id'),
'park_id' => (int) $request->input('park_id'),
'price_parkplaceid' => $request->input('price_parkplaceid'),
'psection_id' => $request->input('psection_id'),
'reserve_date' => $request->input('reserve_date'),
'reserve_start' => $request->input('reserve_start'),
'reserve_end' => $request->input('reserve_end'),
'valid_flag' => $request->input('valid_flag'),
'ope_id' => $request->input('ope_id'),
'updated_at' => now(),
];
DB::table('reserve')->where('reserve_id', $id)->update($data);
return redirect()->route('reserves')->with('success', '予約を更新しました。');
}
// GET: 編集画面表示用の各種プルダウン(存在するテーブルだけ読む)
$userOptions = DB::table('user')
->orderBy('user_id', 'asc')
->limit(5000)
->pluck(DB::raw("concat(user_id, ' ', user_name)"), 'user_id')
->toArray();
$parkOptions = DB::table('park')
->orderBy('park_id', 'asc')
->limit(5000)
->pluck(DB::raw("concat(park_id, ' ', park_name)"), 'park_id')
->toArray();
$userTypeOptions = Schema::hasTable('usertype')
? DB::table('usertype')->orderBy('user_categoryid')
->pluck('print_name', 'user_categoryid')->toArray()
: [];
$parkplaceOptions = Schema::hasTable('price_parkplace')
? DB::table('price_parkplace')->orderBy('price_parkplaceid')
->pluck('price_parkplaceid', 'price_parkplaceid')->toArray()
: [];
$psectionOptions = Schema::hasTable('psection')
? DB::table('psection')->orderBy('psection_id')
->pluck(
Schema::hasColumn('psection', 'psection_name') ? 'psection_name' : 'psection_id',
'psection_id'
)->toArray()
: [];
$ptypeOptions = Schema::hasTable('ptype')
? DB::table('ptype')->orderBy('ptype_id')
->pluck(
Schema::hasColumn('ptype', 'ptype_name') ? 'ptype_name' : 'ptype_id',
'ptype_id'
)->toArray()
: [];
return view('admin.reserves.edit', compact(
'row',
'userOptions',
'parkOptions',
'userTypeOptions',
'parkplaceOptions',
'psectionOptions',
'ptypeOptions'
));
}
} }

View File

@ -1,78 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class SealsController extends Controller
{
public function list(Request $request)
{
$q = \DB::table('seal as s')
->leftJoin('park as p','s.park_id','=','p.park_id')
->leftJoin('regular_contract as rc','s.contract_id','=','rc.contract_id')
->leftJoin('psection as ps','rc.psection_id','=','ps.psection_id')
->select([
's.seal_issueid',
's.park_id',
'p.park_name',
's.contract_id',
'rc.psection_id',
\DB::raw('ps.psection_subject AS psection_subject'),
'rc.contract_seal_issue',
's.seal_day',
's.seal_reason',
]);
// 駐輪場フィルタ
if($request->filled('park_id')){
$q->where('s.park_id',$request->park_id);
}
// 期間フィルタ
$periodType = $request->input('period_type','range');
if($periodType === 'range'){
if($request->filled('seal_day_from')){
$q->whereDate('s.seal_day','>=',$request->seal_day_from);
}
if($request->filled('seal_day_to')){
$q->whereDate('s.seal_day','<=',$request->seal_day_to);
}
} elseif($periodType === 'recent' && $request->filled('recent_period')){
$map = ['12m'=>12,'6m'=>6,'3m'=>3,'2m'=>2,'1m'=>1,'1w'=>'1w'];
$key = $request->recent_period;
if(isset($map[$key])){
$from = $key==='1w' ? now()->subWeek() : now()->subMonths($map[$key]);
$q->where('s.seal_day','>=',$from->toDateString());
}
}
// --- 並び替え: パラメータがある場合のみ適用 ---
$sort = $request->query('sort'); // デフォルト null
$sortType = $request->query('sort_type','asc'); // 指定なければ asc
$allow = [
'seal_issueid' => 's.seal_issueid',
'park_id' => 'p.park_name',
'contract_id' => 's.contract_id',
'seal_day' => 's.seal_day',
'contract_seal_issue' => 'rc.contract_seal_issue',
'psection_subject' => 'ps.psection_subject',
];
if(isset($allow[$sort])){
$sortType = $sortType === 'desc' ? 'desc' : 'asc';
$q->orderBy($allow[$sort], $sortType);
}
// 並び替え指定が無い時は orderBy 不要 → DB の物理(主キー)順
$list = $q->paginate(20)->appends($request->query());
$parks = \DB::table('park')
->select('park_id','park_name')
->orderBy('park_name')
->get();
return view('admin.seals.list', compact('list','parks','sort','sortType'));
}
}

View File

@ -16,43 +16,25 @@ class SettingController extends Controller
public function list(Request $request) public function list(Request $request)
{ {
$perPage = \App\Utils::item_per_page ?? 20; $perPage = \App\Utils::item_per_page ?? 20;
$list = Setting::orderBy('setting_id', 'desc')->paginate($perPage);
// リクエストから取得
$sort = $request->input('sort', 'setting_id');
$sort_type = $request->input('sort_type', 'asc');
// 許可されたカラムのみソート(安全対策)
$allowedSorts = ['setting_id', 'setting_key', 'setting_value']; // ← 必要に応じて増やす
if (!in_array($sort, $allowedSorts)) {
$sort = 'setting_id';
}
if (!in_array($sort_type, ['asc', 'desc'])) {
$sort_type = 'desc';
}
$list = Setting::orderBy($sort, $sort_type)->paginate($perPage);
return view('admin.settings.list', [ return view('admin.settings.list', [
'list' => $list, 'list' => $list,
'sort' => $sort, 'sort' => 'setting_id',
'sort_type' => $sort_type, 'sort_type' => 'desc',
]); ]);
} }
/** /**
* 新規追加GET/POST: /settings/add * 追加GET: 画面 / POST: 登録): /settings/add
*/ */
public function add(Request $request) public function add(Request $request)
{ {
if ($request->isMethod('post')) { if ($request->isMethod('post')) {
$v = Validator::make($request->all(), $this->rules()); $v = Validator::make($request->all(), $this->rules());
if ($v->fails()) { if ($v->fails()) return back()->withErrors($v)->withInput();
return back()->withErrors($v)->withInput();
}
// チェックボックス(未送信時は false // チェックボックス(未送信時は 0
$data = $this->onlyFillable($request); $data = $this->onlyFillable($request);
$data['printable_alert_flag'] = $request->boolean('printable_alert_flag'); $data['printable_alert_flag'] = $request->boolean('printable_alert_flag');
@ -63,15 +45,15 @@ class SettingController extends Controller
return redirect()->route('settings')->with('success', '設定を登録しました。'); return redirect()->route('settings')->with('success', '設定を登録しました。');
} }
// GET時空フォーム表示
return view('admin.settings.add', [ return view('admin.settings.add', [
'setting' => new Setting(), // フォーム初期化用 'setting' => new Setting(), // フォーム初期化用
'isInfo' => false,
'isEdit' => false, 'isEdit' => false,
]); ]);
} }
/** /**
* 編集GET/POST: /settings/edit/{id} * 編集GET: 画面 / POST: 更新: /settings/edit/{id}
*/ */
public function edit(Request $request, int $id) public function edit(Request $request, int $id)
{ {
@ -79,9 +61,7 @@ class SettingController extends Controller
if ($request->isMethod('post')) { if ($request->isMethod('post')) {
$v = Validator::make($request->all(), $this->rules($id)); $v = Validator::make($request->all(), $this->rules($id));
if ($v->fails()) { if ($v->fails()) return back()->withErrors($v)->withInput();
return back()->withErrors($v)->withInput();
}
$data = $this->onlyFillable($request); $data = $this->onlyFillable($request);
$data['printable_alert_flag'] = $request->boolean('printable_alert_flag'); $data['printable_alert_flag'] = $request->boolean('printable_alert_flag');
@ -93,14 +73,13 @@ class SettingController extends Controller
return redirect()->route('settings')->with('success', '設定を更新しました。'); return redirect()->route('settings')->with('success', '設定を更新しました。');
} }
// GET時編集フォーム表示
return view('admin.settings.edit', [ return view('admin.settings.edit', [
'setting' => $setting, 'setting' => $setting,
'isInfo' => false,
'isEdit' => true, 'isEdit' => true,
]); ]);
} }
/** /**
* 詳細表示: /settings/info/{id} * 詳細表示: /settings/info/{id}
*/ */
@ -116,30 +95,25 @@ class SettingController extends Controller
} }
/** /**
* 削除(一覧/編集 共通対応): /settings/delete または /settings/delete/{id} * 削除(単体/複数対応): /settings/delete
*/ */
public function delete(Request $request, $id = null) public function delete(Request $request)
{ {
// 一覧画面checkbox で複数削除)
$ids = $request->input('ids'); $ids = $request->input('ids');
$id = $request->input('id');
if ($id) $ids = [$id];
// 編集画面(単体削除) if (!is_array($ids) || empty($ids)) {
if ($id) { return back()->with('error', '削除対象が指定されていません。');
$ids = [$id];
} }
// 削除対象が空 DB::transaction(function () use ($ids) {
if (empty($ids)) { Setting::whereIn('setting_id', $ids)->delete();
return redirect()->route('settings')->with('error', '削除対象が選択されていません。'); });
}
// 削除処理
Setting::destroy($ids);
return redirect()->route('settings')->with('success', '設定を削除しました。'); return redirect()->route('settings')->with('success', '設定を削除しました。');
} }
// ===== バリデーション・ユーティリティ ===== // ===== バリデーション・ユーティリティ =====
/** /**
@ -152,7 +126,7 @@ class SettingController extends Controller
'web_master' => ['nullable','string','max:255'], 'web_master' => ['nullable','string','max:255'],
'auto_change_date' => ['nullable','date'], 'auto_change_date' => ['nullable','date'],
'auto_chage_master' => ['nullable','string','max:255'], // ※カラム名は仕様通り 'auto_chage_master' => ['nullable','string','max:255'], // ※カラム名は仕様通り
're-issue_alert_number' => ['nullable','integer','min:0'], 're_issue_alert_number' => ['nullable','integer','min:0'],
'image_base_url1' => ['nullable','string','max:255'], 'image_base_url1' => ['nullable','string','max:255'],
'image_base_url2' => ['nullable','string','max:255'], 'image_base_url2' => ['nullable','string','max:255'],
'printable_alert_flag' => ['nullable','boolean'], 'printable_alert_flag' => ['nullable','boolean'],
@ -173,7 +147,7 @@ class SettingController extends Controller
'web_master', 'web_master',
'auto_change_date', 'auto_change_date',
'auto_chage_master', 'auto_chage_master',
're-issue_alert_number', 're_issue_alert_number',
'image_base_url1', 'image_base_url1',
'image_base_url2', 'image_base_url2',
'printable_number', 'printable_number',

View File

@ -16,14 +16,9 @@ class SettlementTransactionController extends Controller
*/ */
public function list(Request $request) public function list(Request $request)
{ {
// 解除ボタンが押された場合 → 一覧にリダイレクトして検索条件リセット
if ($request->input('action') === 'unlink') {
return redirect()->route('settlement_transactions');
}
$q = SettlementTransaction::query(); $q = SettlementTransaction::query();
// --- 絞り込み // --- 絞り込み(必要なら増やせます)
$contractId = $request->input('contract_id'); $contractId = $request->input('contract_id');
$status = trim((string)$request->input('status', '')); $status = trim((string)$request->input('status', ''));
$from = $request->input('from'); // 支払日時 from $from = $request->input('from'); // 支払日時 from
@ -64,7 +59,6 @@ class SettlementTransactionController extends Controller
]); ]);
} }
/** /**
* 新規 * 新規
* ルート: settlement_transactions_add * ルート: settlement_transactions_add

View File

@ -1,229 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Station;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\DB;
use App\Models\Park;
class StationController extends Controller
{
/**
* 一覧表示
*/
public function list(Request $request)
{
$sort = $request->input('sort', 'station_id');
$sort_type = $request->input('sort_type', 'asc');
$allowedSorts = [
'station_id',
'park_id',
'station_neighbor_station',
'station_name_ruby',
'station_route_name'
];
if (!in_array($sort, $allowedSorts)) {
$sort = 'station_id';
}
if (!in_array($sort_type, ['asc', 'desc'])) {
$sort_type = 'asc';
}
$list = Station::select([
'station_id',
'station_neighbor_station',
'station_name_ruby',
'station_route_name',
'park_id',
'operator_id',
'station_latitude',
'station_longitude',
])
->orderBy($sort, $sort_type)
->paginate(20);
return view('admin.stations.list', compact('list', 'sort', 'sort_type'));
}
public function add(Request $request)
{
if ($request->isMethod('get')) {
// 駐車場リストを取得(プルダウン用)
$parks = Park::orderBy('park_name')->pluck('park_name', 'park_id');
// 新規時:空レコードを渡す
return view('admin.stations.add', [
'isEdit' => false,
'record' => new Station(),
'parks' => $parks, // ← これを追加
]);
}
// POST時バリデーション
$rules = [
'station_neighbor_station' => 'required|string|max:255',
'station_name_ruby' => 'required|string|max:255',
'station_route_name' => 'required|string|max:255',
'station_latitude' => 'required|numeric',
'station_longitude' => 'required|numeric',
'operator_id' => 'nullable|integer',
'park_id' => 'required|integer',
];
$messages = [
'station_latitude.required' => '緯度は必須項目です。',
'station_longitude.required' => '経度は必須項目です。',
];
$validator = Validator::make($request->all(), $rules, $messages);
if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput();
}
DB::transaction(function () use ($request) {
Station::create($request->only([
'station_neighbor_station',
'station_name_ruby',
'station_route_name',
'station_latitude',
'station_longitude',
'park_id',
'operator_id',
]));
});
return redirect()->route('stations')->with('success', '登録しました。');
}
/**
* 編集(画面/処理)
*/
public function edit(Request $request, $id)
{
$record = Station::findOrFail($id);
if ($request->isMethod('get')) {
// 駐車場リストを取得(プルダウン用)
$parks = Park::orderBy('park_name')->pluck('park_name', 'park_id');
return view('admin.stations.edit', [
'isEdit' => true,
'record' => $record,
'parks' => $parks, // ← ここを追加
]);
}
// ▼ POST時バリデーション
$rules = [
'station_neighbor_station' => 'required|string|max:255',
'station_name_ruby' => 'required|string|max:255',
'station_route_name' => 'required|string|max:255',
'station_latitude' => 'required|numeric',
'station_longitude' => 'required|numeric',
'operator_id' => 'nullable|integer',
'park_id' => 'required|integer',
];
$messages = [
'station_latitude.required' => '緯度は必須項目です。',
'station_longitude.required' => '経度は必須項目です。',
];
$validator = Validator::make($request->all(), $rules, $messages);
if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput();
}
DB::transaction(function () use ($request, $record) {
$record->update($request->only([
'station_neighbor_station',
'station_name_ruby',
'station_route_name',
'park_id',
'operator_id',
'station_latitude',
'station_longitude',
]));
});
return redirect()->route('stations')->with('success', '更新しました。');
}
/**
* 削除(単一/複数対応)
*/
public function delete(Request $request)
{
$ids = [];
if ($request->filled('id')) {
$ids[] = (int) $request->input('id');
}
if (is_array($request->input('pk'))) {
$ids = array_merge($ids, $request->input('pk'));
}
$ids = array_values(array_unique(array_map('intval', $ids)));
if (empty($ids)) {
return back()->with('error', '削除対象が選択されていません。');
}
Station::whereIn('station_id', $ids)->delete();
return redirect()->route('stations')->with('success', '削除しました');
}
/**
* CSVインポート
*/
public function import(Request $request)
{
// TODO: 実装予定
return redirect()->route('stations')->with('info', 'CSVインポートは未実装です');
}
/**
* CSVエクスポート日本語ヘッダー付き
*/
public function export()
{
// ファイル名
$filename = '近傍駅マスタ_' . now()->format('YmdHis') . '.csv';
return response()->streamDownload(function () {
// Excel用 UTF-8 BOM
echo "\xEF\xBB\xBF";
// 日本語ヘッダー行
echo "近傍駅ID,駐車場ID,近傍駅,近傍駅ふりがな,路線名,近傍駅座標(緯度),近傍駅座標(経度)\n";
// データ行
foreach (\App\Models\Station::all() as $station) {
echo implode(',', [
$station->station_id,
$station->park_id,
$station->station_neighbor_station,
$station->station_name_ruby,
$station->station_route_name,
$station->station_latitude,
$station->station_longitude,
// $station->operator_id,
]) . "\n";
}
}, $filename);
}
}

View File

@ -1,187 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Barryvdh\DomPDF\Facade\Pdf;
class TagissueController extends Controller
{
// タグ発送宛名PDF生成
public function printUnissuedLabels(Request $request)
{
$ids = $request->input('ids', []);
if (empty($ids) || !is_array($ids)) {
return back()->with('error', '1件以上選択してください。');
}
// 利用者情報取得
$users = DB::table('user')
->whereIn('user_id', $ids)
->select('user_name', 'user_regident_zip', 'user_regident_pre')
->get();
// PDF生成Laravel-dompdf使用例
$pdfHtml = view('admin.tag_issue.pdf_labels', ['users' => $users])->render();
$pdf = Pdf::setOptions(['isRemoteEnabled' => true])->loadHTML($pdfHtml);
return $pdf->download('tag_labels.pdf');
}
public function list(Request $request)
{
// 絞り込み条件
$filterType = $request->input('filter_type'); // 'unissued', 'issued', 'all', 'unissued_toggle'
$tagSerial = $request->input('tag_serial');
$tagSerial64 = $request->input('tag_serial_64');
// ソートパラメータ取得
$sort = $request->input('sort');
$sortType = $request->input('sort_type', 'asc');
// userテーブルとoperator_queテーブルをJOIN
$query = DB::table('user')
->leftJoin('operator_que', 'user.user_id', '=', 'operator_que.user_id')
->select(
'user.user_seq',
'user.user_id',
'user.user_tag_serial',
'user.user_tag_serial_64',
'user.user_tag_issue',
'user.user_name',
'user.user_mobile',
'user.user_homephone',
'user.user_regident_zip',
'user.user_regident_pre',
'user.user_regident_city',
'user.user_regident_add',
'user.user_relate_zip',
'user.user_relate_pre',
'user.user_relate_city',
'user.user_relate_add',
'operator_que.que_id',
'operator_que.que_class',
'operator_que.que_status'
);
// ソート項目に応じたorderBy
switch ($sort) {
case 'que_id':
$query->orderBy('operator_que.que_id', $sortType);
break;
case 'user_tag_serial':
$query->orderBy('user.user_tag_serial', $sortType);
break;
case 'user_tag_serial_64':
$query->orderBy('user.user_tag_serial_64', $sortType);
break;
case 'user_tag_issue':
$query->orderBy('user.user_tag_issue', $sortType);
break;
case 'user_name':
$query->orderBy('user.user_name', $sortType);
break;
default:
$query->orderByDesc('user.user_seq');
}
// 【種別フィルター】タグ未発送(通常)
if ($filterType === 'unissued') {
$query->where('operator_que.que_class', 3)
->where('operator_que.que_status', 1);
}
// 【種別フィルター】タグ未発送トグルque_class=3かつque_status≠3
if ($filterType === 'unissued_toggle') {
$query->where('operator_que.que_class', 3)
->where('operator_que.que_status', '<>', 3);
}
// 【種別フィルター】タグ発送済み(通常)
if ($filterType === 'issued') {
$query->where('operator_que.que_class', 3)
->where('operator_que.que_status', 3);
}
// 【種別フィルター】タグ発送済みトグルque_class=3 and que_status=3
if ($filterType === 'issued_toggle') {
$query->where('operator_que.que_class', 3)
->where('operator_que.que_status', 3);
}
// 【タグシリアル・タグシリアル64進フィルター】
if (!empty($tagSerial)) {
$query->where('user.user_tag_serial', 'like', "%$tagSerial%");
}
if (!empty($tagSerial64)) {
$query->where('user.user_tag_serial_64', 'like', "%$tagSerial64%");
}
$users = $query->paginate(20);
return view('admin.tag_issue.list', [
'users' => $users,
'filterType' => $filterType,
'tagSerial' => $tagSerial,
'tagSerial64' => $tagSerial64,
'sort' => $sort,
'sortType' => $sortType,
]);
}
// ステータス変更(タグ発送済み/未発送)
public function updateStatus(Request $request)
{
$ids = $request->input('ids', []); // チェックされたuser_id配列
$action = $request->input('action'); // 'to_issued' or 'to_unissued'
$operatorId = auth()->id();
$now = now();
// 対象ユーザーのoperator_queを取得
$ques = DB::table('operator_que')->whereIn('user_id', $ids)->get();
if ($action === 'to_issued') {
$alreadyIssued = $ques->where('que_status', 3)->pluck('user_id')->toArray();
if (count($alreadyIssued) > 0) {
return back()->with('error', 'すでにタグ発送済みのユーザーが含まれています。');
}
// 確認ダイアログはJS側で
DB::table('operator_que')->whereIn('user_id', $ids)
->update(['que_status' => 3, 'updated_at' => $now, 'operator_id' => $operatorId]);
return back()->with('success', 'ステータスをタグ発送済に変更しました。');
}
if ($action === 'to_unissued') {
$alreadyUnissued = $ques->where('que_status', 1)->pluck('user_id')->toArray();
if (count($alreadyUnissued) > 0) {
// 既にタグ未発送のユーザー名を取得
$names = DB::table('user')->whereIn('user_id', $alreadyUnissued)->pluck('user_name')->toArray();
return back()->with('error', 'すでにタグ未発送のユーザーが含まれています: ' . implode(', ', $names));
}
// すべてque_status=1以外なので更新
DB::table('operator_que')->whereIn('user_id', $ids)
->update(['que_status' => 1, 'updated_at' => $now, 'operator_id' => $operatorId]);
return back()->with('success', 'ステータスをタグ未発送に変更しました。');
}
return back()->with('error', '不正な操作です。');
}
// Ajax: 選択user_idのque_statusを返すタグ発送済み/未発送判定)
public function checkStatus(Request $request)
{
$ids = $request->input('ids', []);
$type = $request->input('type'); // 'issued' or 'unissued'
$users = DB::table('user')
->leftJoin('operator_que', 'user.user_id', '=', 'operator_que.user_id')
->whereIn('user.user_id', $ids)
->select('user.user_name', 'operator_que.que_status')
->get();
$alreadyIssued = [];
$alreadyUnissued = [];
foreach ($users as $u) {
if ($type === 'issued' && $u->que_status == 3) {
$alreadyIssued[] = $u->user_name;
}
if ($type === 'unissued' && $u->que_status == 1) {
$alreadyUnissued[] = $u->user_name;
}
}
return response()->json([
'alreadyIssued' => $alreadyIssued,
'alreadyUnissued' => $alreadyUnissued
]);
}
}

View File

@ -21,6 +21,7 @@ class TaxController extends Controller
// 絞り込み // 絞り込み
$keyword = trim((string) $request->input('kw')); $keyword = trim((string) $request->input('kw'));
if ($keyword !== '') { if ($keyword !== '') {
// 数値型でも互換のため部分一致を残す
$query->where('tax_percent', 'like', "%{$keyword}%"); $query->where('tax_percent', 'like', "%{$keyword}%");
} }
$from = $request->input('from'); $from = $request->input('from');
@ -32,15 +33,15 @@ class TaxController extends Controller
$query->whereDate('tax_day', '<=', $to); $query->whereDate('tax_day', '<=', $to);
} }
// ソート(既定:ID 昇順) // ソート(既定:適用日 降順)
$sort = $request->input('sort', 'tax_id'); $sort = $request->input('sort', 'tax_day');
$type = strtolower($request->input('sort_type', 'asc')); $type = strtolower($request->input('sort_type', 'desc'));
$allow = ['tax_day', 'tax_percent', 'updated_at', 'created_at', 'tax_id']; $allow = ['tax_day', 'tax_percent', 'updated_at', 'created_at', 'tax_id'];
if (!in_array($sort, $allow, true)) { if (!in_array($sort, $allow, true)) {
$sort = 'tax_id'; $sort = 'tax_day';
} }
if (!in_array($type, ['asc', 'desc'], true)) { if (!in_array($type, ['asc', 'desc'], true)) {
$type = 'asc'; $type = 'desc';
} }
$query->orderBy($sort, $type); $query->orderBy($sort, $type);
@ -56,19 +57,18 @@ class TaxController extends Controller
]); ]);
} }
public function add(Request $request) public function add(Request $request)
{ {
if ($request->isMethod('post')) { if ($request->isMethod('post')) {
$data = $request->validate([ $data = $request->validate([
'tax_percent' => ['required', 'numeric', 'min:0', 'max:1000'], 'tax_percent' => ['required', 'numeric', 'min:0', 'max:1000'],
'tax_day' => ['required', 'date'], 'tax_day' => ['required', 'date', 'unique:tax,tax_day'],
]); ]);
$data['operator_id'] = optional(\Auth::user())->ope_id ?? null; $data['operator_id'] = optional(\Auth::user())->ope_id ?? null;
$data['tax_percent'] = number_format((float)$data['tax_percent'], 2, '.', ''); $data['tax_percent'] = number_format((float)$data['tax_percent'], 2, '.', '');
\App\Models\Tax::create($data); \App\Models\Tax::create($data);
return redirect()->route('tax')->with('success', '登録しました'); return redirect()->route('tax')->with('success', '登録しました');
} }
return view('admin.tax.add', [ return view('admin.tax.add', [
@ -76,22 +76,22 @@ class TaxController extends Controller
'isEdit' => false, 'isEdit' => false,
'isInfo' => false, 'isInfo' => false,
]); ]);
} }
public function edit(int $tax_id, Request $request) public function edit(int $tax_id, Request $request)
{ {
$tax = \App\Models\Tax::findOrFail($tax_id); $tax = \App\Models\Tax::findOrFail($tax_id);
if ($request->isMethod('post')) { if ($request->isMethod('post')) {
$data = $request->validate([ $data = $request->validate([
'tax_percent' => ['required', 'numeric', 'min:0', 'max:1000'], 'tax_percent' => ['required', 'numeric', 'min:0', 'max:1000'],
'tax_day' => ['required', 'date'], 'tax_day' => ['required', 'date', 'unique:tax,tax_day,' . $tax->tax_id . ',tax_id'],
]); ]);
$data['operator_id'] = optional(\Auth::user())->ope_id ?? null; $data['operator_id'] = optional(\Auth::user())->ope_id ?? null;
$data['tax_percent'] = number_format((float)$data['tax_percent'], 2, '.', ''); $data['tax_percent'] = number_format((float)$data['tax_percent'], 2, '.', '');
$tax->update($data); $tax->update($data);
return redirect()->route('tax')->with('success', '更新しました'); return redirect()->route('tax')->with('success', '更新しました');
} }
return view('admin.tax.edit', [ return view('admin.tax.edit', [
@ -99,10 +99,10 @@ class TaxController extends Controller
'isEdit' => true, 'isEdit' => true,
'isInfo' => false, 'isInfo' => false,
]); ]);
} }
public function info(int $tax_id) public function info(int $tax_id)
{ {
$tax = \App\Models\Tax::findOrFail($tax_id); $tax = \App\Models\Tax::findOrFail($tax_id);
return view('admin.tax.info', [ return view('admin.tax.info', [
@ -110,7 +110,7 @@ class TaxController extends Controller
'isEdit' => false, 'isEdit' => false,
'isInfo' => true, 'isInfo' => true,
]); ]);
} }
/** /**
@ -119,175 +119,166 @@ class TaxController extends Controller
*/ */
public function delete(Request $request) public function delete(Request $request)
{ {
$ids = (array) $request->input('ids', []);
$pk = $request->input('pk', []);
// 配列に統一
$ids = is_array($pk) ? $pk : [$pk];
// 数字チェック
$ids = array_values(array_filter($ids, fn($v) => preg_match('/^\d+$/', (string) $v))); $ids = array_values(array_filter($ids, fn($v) => preg_match('/^\d+$/', (string) $v)));
if (empty($ids)) { if (empty($ids)) {
return redirect()->route('tax')->with('error', '削除対象が選択されていません。'); return redirect()->route('tax')->with('error', '削除対象が選択されていません。');
} }
// 削除
Tax::whereIn('tax_id', $ids)->delete(); Tax::whereIn('tax_id', $ids)->delete();
return redirect()->route('tax')->with('success', '削除しました'); return redirect()->route('tax')->with('success', '削除しました');
} }
/**
* CSVインポート
* カラム想定: tax_percent, tax_day
* - 1行目はヘッダ可
* - tax_day をキーとして「存在すれば更新 / 無ければ作成」
*/
public function import(Request $request)
{
$request->validate([
'file' => ['required', 'file', 'mimetypes:text/plain,text/csv,text/tsv', 'max:2048'],
]);
$path = $request->file('file')->getRealPath();
if (!$path || !is_readable($path)) {
return redirect()->route('tax')->with('error', 'ファイルを読み込めません。');
}
// /** $created = 0;
// * CSVインポート $updated = 0;
// * カラム想定: tax_percent, tax_day $skipped = 0;
// * - 1行目はヘッダ可
// * - tax_day をキーとして「存在すれば更新 / 無ければ作成」
// */
// public function import(Request $request)
// {
// $request->validate([
// 'file' => ['required', 'file', 'mimetypes:text/plain,text/csv,text/tsv', 'max:2048'],
// ]);
// $path = $request->file('file')->getRealPath(); DB::beginTransaction();
// if (!$path || !is_readable($path)) { try {
// return redirect()->route('tax')->with('error', 'ファイルを読み込めません。'); if (($fp = fopen($path, 'r')) !== false) {
// } $line = 0;
while (($row = fgetcsv($fp)) !== false) {
$line++;
// $created = 0; // 空行スキップ
// $updated = 0; if (count($row) === 1 && trim((string) $row[0]) === '') {
// $skipped = 0; continue;
}
// DB::beginTransaction(); // ヘッダ行っぽい場合1行目に 'tax_percent' を含む)
// try { if ($line === 1) {
// if (($fp = fopen($path, 'r')) !== false) { $joined = strtolower(implode(',', $row));
// $line = 0; if (str_contains($joined, 'tax_percent') && str_contains($joined, 'tax_day')) {
// while (($row = fgetcsv($fp)) !== false) { continue; // ヘッダスキップ
// $line++; }
}
// // 空行スキップ // 取り出し(列数が足りない場合スキップ)
// if (count($row) === 1 && trim((string) $row[0]) === '') { $percent = $row[0] ?? null;
// continue; $day = $row[1] ?? null;
// } if ($percent === null || $day === null) {
$skipped++;
continue;
}
// // ヘッダ行っぽい場合1行目に 'tax_percent' を含む) // 正規化 & 検証
// if ($line === 1) { $percent = trim((string) $percent);
// $joined = strtolower(implode(',', $row)); $percent = rtrim($percent, '%');
// if (str_contains($joined, 'tax_percent') && str_contains($joined, 'tax_day')) { $percent = preg_replace('/[^\d.]/', '', $percent) ?? '0';
// continue; // ヘッダスキップ $percentF = (float) $percent;
// } if ($percentF < 0) {
// } $skipped++;
continue;
}
$percentF = (float) number_format($percentF, 2, '.', '');
// // 取り出し(列数が足りない場合スキップ) $day = date('Y-m-d', strtotime((string) $day));
// $percent = $row[0] ?? null; if (!$day) {
// $day = $row[1] ?? null; $skipped++;
// if ($percent === null || $day === null) { continue;
// $skipped++; }
// continue;
// }
// // 正規化 & 検証 // upsert: 適用日ユニーク運用
// $percent = trim((string) $percent); $existing = Tax::whereDate('tax_day', $day)->first();
// $percent = rtrim($percent, '%'); $payload = [
// $percent = preg_replace('/[^\d.]/', '', $percent) ?? '0'; 'tax_percent' => $percentF,
// $percentF = (float) $percent; 'tax_day' => $day,
// if ($percentF < 0) { 'operator_id' => optional(Auth::user())->ope_id ?? null,
// $skipped++; ];
// continue;
// }
// $percentF = (float) number_format($percentF, 2, '.', '');
// $day = date('Y-m-d', strtotime((string) $day)); if ($existing) {
// if (!$day) { $existing->update($payload);
// $skipped++; $updated++;
// continue; } else {
// } Tax::create($payload);
$created++;
}
}
fclose($fp);
}
// // upsert: 適用日ユニーク運用 DB::commit();
// $existing = Tax::whereDate('tax_day', $day)->first(); return redirect()->route('tax')->with('success', "インポート完了:新規 {$created} 件、更新 {$updated} 件、スキップ {$skipped}");
// $payload = [ } catch (\Throwable $e) {
// 'tax_percent' => $percentF, DB::rollBack();
// 'tax_day' => $day, return redirect()->route('tax')->with('error', 'インポートに失敗しました:' . $e->getMessage());
// 'operator_id' => optional(Auth::user())->ope_id ?? null, }
// ]; }
// if ($existing) { /**
// $existing->update($payload); * CSVエクスポート現在の絞り込み/ソート条件を反映
// $updated++; */
// } else { public function export(Request $request): StreamedResponse
// Tax::create($payload); {
// $created++; $query = Tax::query();
// }
// }
// fclose($fp);
// }
// DB::commit(); $keyword = trim((string) $request->input('kw'));
// return redirect()->route('tax')->with('success', "インポート完了:新規 {$created} 件、更新 {$updated} 件、スキップ {$skipped} 件"); if ($keyword !== '') {
// } catch (\Throwable $e) { $query->where('tax_percent', 'like', "%{$keyword}%");
// DB::rollBack(); }
// return redirect()->route('tax')->with('error', 'インポートに失敗しました:' . $e->getMessage()); $from = $request->input('from');
// } $to = $request->input('to');
// } if ($from) {
$query->whereDate('tax_day', '>=', $from);
}
if ($to) {
$query->whereDate('tax_day', '<=', $to);
}
// /** $sort = $request->input('sort', 'tax_day');
// * CSVエクスポート現在の絞り込み/ソート条件を反映 $type = strtolower($request->input('sort_type', 'desc'));
// */ $allow = ['tax_day', 'tax_percent', 'updated_at', 'created_at', 'tax_id'];
// public function export(Request $request): StreamedResponse if (!in_array($sort, $allow, true)) {
// { $sort = 'tax_day';
// $query = Tax::query(); }
if (!in_array($type, ['asc', 'desc'], true)) {
$type = 'desc';
}
$query->orderBy($sort, $type);
// $keyword = trim((string) $request->input('kw')); $filename = 'tax_' . now()->format('Ymd_His') . '.csv';
// if ($keyword !== '') {
// $query->where('tax_percent', 'like', "%{$keyword}%");
// }
// $from = $request->input('from');
// $to = $request->input('to');
// if ($from) {
// $query->whereDate('tax_day', '>=', $from);
// }
// if ($to) {
// $query->whereDate('tax_day', '<=', $to);
// }
// $sort = $request->input('sort', 'tax_day'); return response()->streamDownload(function () use ($query) {
// $type = strtolower($request->input('sort_type', 'desc')); $out = fopen('php://output', 'w');
// $allow = ['tax_day', 'tax_percent', 'updated_at', 'created_at', 'tax_id']; // Header設計書の主要カラム
// if (!in_array($sort, $allow, true)) { fputcsv($out, ['消費税ID', '消費税率', '適用日', '登録日時', '更新日時', '更新オペレータID']);
// $sort = 'tax_day'; $query->chunk(500, function ($rows) use ($out) {
// } foreach ($rows as $r) {
// if (!in_array($type, ['asc', 'desc'], true)) { fputcsv($out, [
// $type = 'desc'; $r->tax_id,
// } // 画面仕様に合わせたい場合は getDisplayTaxPercentAttribute() に置換可
// $query->orderBy($sort, $type); is_numeric($r->tax_percent)
? number_format((float) $r->tax_percent, 2, '.', '')
// $filename = 'tax_' . now()->format('Ymd_His') . '.csv'; : (string) $r->tax_percent,
optional($r->tax_day)->format('Y-m-d'),
// return response()->streamDownload(function () use ($query) { optional($r->created_at)->format('Y-m-d H:i:s'),
// $out = fopen('php://output', 'w'); optional($r->updated_at)->format('Y-m-d H:i:s'),
// // Header設計書の主要カラム $r->operator_id,
// fputcsv($out, ['消費税ID', '消費税率', '適用日', '登録日時', '更新日時', '更新オペレータID']); ]);
// $query->chunk(500, function ($rows) use ($out) { }
// foreach ($rows as $r) { });
// fputcsv($out, [ fclose($out);
// $r->tax_id, }, $filename, [
// // 画面仕様に合わせたい場合は getDisplayTaxPercentAttribute() に置換可 'Content-Type' => 'text/csv; charset=UTF-8',
// is_numeric($r->tax_percent) ]);
// ? number_format((float) $r->tax_percent, 2, '.', '') }
// : (string) $r->tax_percent,
// optional($r->tax_day)->format('Y-m-d'),
// optional($r->created_at)->format('Y-m-d H:i:s'),
// optional($r->updated_at)->format('Y-m-d H:i:s'),
// $r->operator_id,
// ]);
// }
// });
// fclose($out);
// }, $filename, [
// 'Content-Type' => 'text/csv; charset=UTF-8',
// ]);
// }
} }

View File

@ -43,19 +43,17 @@ class TermsController extends Controller
{ {
if ($request->isMethod('post')) { if ($request->isMethod('post')) {
$validated = $request->validate([ $validated = $request->validate([
'city_id' => 'required|integer',
'terms_revision' => 'required|string|max:255', 'terms_revision' => 'required|string|max:255',
'terms_text' => 'required|string', 'terms_text' => 'required|string',
'start_date' => 'nullable|date', 'start_date' => 'nullable|date',
'use_flag' => 'required|in:0,1', 'use_flag' => 'required|in:0,1',
'memo' => 'nullable|string|max:255', 'memo' => 'nullable|string|max:255',
'terms_created_at' => 'nullable|date', 'city_id' => 'nullable|integer',
'operator_id' => 'nullable|integer', 'operator_id' => 'nullable|integer',
]); ]);
Term::create($validated); Term::create($validated);
return redirect()->route('terms')->with('success', '登録しました。'); return redirect()->route('terms')->with('success', '利用規約が登録されました');
} }
// 都市の選択肢を取得 // 都市の選択肢を取得
$cities = City::pluck('city_name', 'city_id'); $cities = City::pluck('city_name', 'city_id');
@ -64,6 +62,7 @@ class TermsController extends Controller
} }
// 編集画面・更新処理
public function edit(Request $request, $id) public function edit(Request $request, $id)
{ {
$term = Term::findOrFail($id); $term = Term::findOrFail($id);
@ -71,27 +70,24 @@ class TermsController extends Controller
if ($request->isMethod('post')) { if ($request->isMethod('post')) {
$validated = $request->validate([ $validated = $request->validate([
'city_id' => 'required|integer',
'terms_revision' => 'required|string|max:255', 'terms_revision' => 'required|string|max:255',
'terms_text' => 'required|string', 'terms_text' => 'required|string',
'start_date' => 'nullable|date', 'start_date' => 'nullable|date',
'use_flag' => 'required|in:0,1', 'use_flag' => 'required|in:0,1',
'memo' => 'nullable|string|max:255', 'memo' => 'nullable|string|max:255',
'terms_created_at'=> 'nullable|date', 'city_id' => 'nullable|integer',
'operator_id' => 'nullable|integer', 'operator_id' => 'nullable|integer',
]); ]);
$term->update($validated); $term->update($validated);
return redirect()->route('terms')->with('success', '更新しました。'); return redirect()->route('terms')->with('success', '利用規約が更新されました');
} }
return view('admin.terms.edit', compact('term', 'cities')); return view('admin.terms.edit', compact('term', 'cities'));
} }
// 詳細表示 // 詳細表示
public function info($id) public function info($id)
{ {
@ -99,24 +95,17 @@ class TermsController extends Controller
return view('admin.terms.info', compact('term')); return view('admin.terms.info', compact('term'));
} }
// 削除処理(単一・複数対応 // 削除処理(複数)
public function delete(Request $request) public function delete(Request $request)
{ {
$request->validate([ $ids = $request->input('id', []); // 修正点:'pk' → 'id'
'pk' => 'required',
'pk.*' => 'integer', // 配列なら中身は整数
]);
$arr_pk = $request->input('pk'); if (!empty($ids)) {
$ids = is_array($arr_pk) ? $arr_pk : [$arr_pk]; Term::destroy($ids);
return redirect()->route('terms')->with('success', '削除しました');
$deleted = Term::destroy($ids);
if ($deleted > 0) {
return redirect()->route('terms')->with('success', __('削除しました。'));
} else {
return redirect()->route('terms')->with('error', __('削除に失敗しました。'));
} }
return redirect()->route('terms')->with('error', '削除対象が見つかりません');
} }
// CSVインポート // CSVインポート

View File

@ -14,26 +14,32 @@ class UpdateCandidateController extends Controller
public function list(Request $request) public function list(Request $request)
{ {
$q = DB::table('regular_contract as rc') $q = DB::table('regular_contract as rc')
->leftJoin('user as u','rc.user_id','=','u.user_id')
->leftJoin('park as p','rc.park_id','=','p.park_id')
->leftJoin('psection as ps','rc.psection_id','=','ps.psection_id')
->leftJoin('usertype as ut','u.user_categoryid','=','ut.user_categoryid')
->select([ ->select([
'rc.contract_id', 'rc.contract_id',
'rc.contract_qr_id',
'rc.user_id', 'rc.user_id',
'rc.user_categoryid',
'rc.park_id', 'rc.park_id',
'rc.contract_created_at', 'rc.contract_created_at',
'rc.contract_periods', 'rc.contract_periods',
'rc.contract_periode', 'rc.contract_periode',
'rc.tag_qr_flag', 'rc.tag_qr_flag',
'rc.contract_flag',
'rc.contract_cancel_flag', 'rc.contract_cancel_flag',
'rc.contract_payment_day',
'rc.contract_money',
'rc.billing_amount',
'rc.contract_permission', 'rc.contract_permission',
'rc.contract_manual',
'rc.contract_notice',
'p.park_name',
'u.user_name', 'u.user_name',
'u.user_phonetic', 'u.user_phonetic',
'u.user_mobile', 'u.user_mobile',
'u.user_homephone', 'u.user_homephone',
'u.user_birthdate', 'u.user_primemail',
'u.user_gender', 'u.user_gender',
'u.user_birthdate',
'u.user_regident_zip', 'u.user_regident_zip',
'u.user_regident_pre', 'u.user_regident_pre',
'u.user_regident_city', 'u.user_regident_city',
@ -42,116 +48,57 @@ class UpdateCandidateController extends Controller
'u.user_relate_pre', 'u.user_relate_pre',
'u.user_relate_city', 'u.user_relate_city',
'u.user_relate_add', 'u.user_relate_add',
'u.user_graduate',
'u.user_workplace', 'u.user_workplace',
'u.user_school', 'u.user_school',
'u.user_graduate',
'u.user_reduction',
'u.user_remarks', 'u.user_remarks',
'p.park_name',
DB::raw('ps.psection_subject as vehicle_type'),
DB::raw('ut.usertype_subject1 as user_category1'),
DB::raw('ut.usertype_subject2 as user_category2'),
DB::raw('ut.usertype_subject3 as user_category3'),
DB::raw('rc.contract_seal_issue as seal_issue_count'),
DB::raw('rc.user_securitynum as crime_prevention'),
DB::raw("CASE rc.enable_months
WHEN 1 THEN '月極(1ヶ月)'
WHEN 3 THEN '3ヶ月'
WHEN 6 THEN '6ヶ月'
WHEN 12 THEN '年'
ELSE CONCAT(rc.enable_months,'ヶ月') END as ticket_type"),
]) ])
->where('rc.contract_cancel_flag',0) ->leftJoin('park as p', 'rc.park_id', '=', 'p.park_id')
->where('rc.contract_permission',1) ->leftJoin('user as u', 'rc.user_id', '=', 'u.user_id')
// 追加: 本日以降が有効期限のレコードのみ ->whereNull('rc.update_flag'); // 未更新のみ
->whereDate('rc.contract_periode','>=', now()->toDateString());
// 絞り込み // 対象月による有効期限の絞り込み
if ($request->filled('park_id')) { if ($request->filled('target_month')) {
$q->where('rc.park_id', $request->park_id); $now = now();
switch ($request->input('target_month')) {
case 'last':
$start = $now->copy()->subMonth()->startOfMonth();
$end = $now->copy()->subMonth()->endOfMonth();
break;
case 'this':
$start = $now->copy()->startOfMonth();
$end = $now->copy()->endOfMonth();
break;
case 'next':
$start = $now->copy()->addMonth()->startOfMonth();
$end = $now->copy()->addMonth()->endOfMonth();
break;
case 'after2':
$start = $now->copy()->addMonths(2)->startOfMonth();
$end = $now->copy()->addMonths(2)->endOfMonth();
break;
default:
$start = null;
$end = null;
} }
if ($request->filled('user_id')) { if ($start && $end) {
$q->where('rc.user_id', trim($request->user_id)); $q->whereBetween('rc.contract_periode', [$start->toDateString(), $end->toDateString()]);
}
// 分類名1 完全一致
if ($request->filled('user_category1')) {
$val = trim(mb_convert_kana($request->user_category1,'asKV'));
$q->where('ut.usertype_subject1', $val);
}
// タグシリアル64進 部分一致 (SELECT 不要)
if ($request->filled('user_tag_serial_64')) {
$q->where('u.user_tag_serial_64','like','%'.$request->user_tag_serial_64.'%');
}
// 有効期限:指定日以前
if ($request->filled('contract_periode')) {
$raw = str_replace('/','-',$request->contract_periode);
try {
$target = \Carbon\Carbon::parse($raw)->format('Y-m-d');
$q->whereDate('rc.contract_periode','<=',$target);
} catch (\Exception $e) {}
}
if ($request->filled('user_phonetic')) {
$q->where('u.user_phonetic','like','%'.$request->user_phonetic.'%');
}
if ($request->filled('user_mobile')) {
$like = '%'.$request->user_mobile.'%';
$q->where(function($w) use ($like){
$w->where('u.user_mobile','like',$like)
->orWhere('u.user_homephone','like',$like);
});
}
if ($request->filled('user_primemail')) {
$like = '%'.$request->user_primemail.'%';
$q->where(function($w) use ($like){
$w->where('u.user_primemail','like',$like)
->orWhere('u.user_submail','like',$like);
});
}
if ($request->filled('user_workplace')) {
$q->where('u.user_workplace','like','%'.$request->user_workplace.'%');
}
if ($request->filled('user_school')) {
$q->where('u.user_school','like','%'.$request->user_school.'%');
}
if ($request->filled('tag_qr_flag') && $request->tag_qr_flag!=='') {
$q->where('rc.tag_qr_flag',$request->tag_qr_flag);
}
// 対象月
$target = $request->input('target_month');
if (in_array($target,['last','this','next','after2'],true)) {
$base = now()->startOfMonth();
$offset = ['last'=>-1,'this'=>0,'next'=>1,'after2'=>2][$target];
$m = $base->copy()->addMonths($offset);
if ($target === 'after2') {
// 2か月後「以降」を抽出該当月の月初以降
$q->whereDate('rc.contract_periode', '>=', $m->toDateString());
} else {
$q->whereYear('rc.contract_periode',$m->year)
->whereMonth('rc.contract_periode',$m->month);
} }
} }
// ソートregular_contract の表示順に合わせて contract_id の降順を既定に) $sort = $request->input('sort', 'rc.contract_id');
$sort = $request->input('sort','contract_id'); $sortType = $request->input('sort_type', 'desc');
$sortType = $request->input('sort_type','dac'); $allowSorts = ['rc.contract_id'];
$allow = [ if (!in_array($sort, $allowSorts)) {
'contract_id' => 'rc.contract_id', $sort = 'rc.contract_id';
'user_id' => 'rc.user_id', }
'contract_periode' => 'rc.contract_periode', $sortType = ($sortType === 'asc') ? 'asc' : 'desc';
];
if (!isset($allow[$sort])) $sort = 'contract_id';
$sortType = $sortType==='desc' ? 'desc' : 'asc';
$q->orderBy($allow[$sort], $sortType);
$rows = $q->paginate(20)->appends($request->query()); $rows = $q->orderBy($sort, $sortType)->paginate(20)->withQueryString();
$parks = DB::table('park') // 駐輪場リスト(プルダウン用)
->select('park_id','park_name') $parks = DB::table('park')->select('park_id', 'park_name')->orderBy('park_name')->get();
->orderBy('park_name')
->get();
return view('admin.update_candidate.list', return view('admin.update_candidate.list', compact('rows', 'sort', 'sortType', 'parks'));
compact('rows','parks'));
} }
} }

View File

@ -6,69 +6,8 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpFoundation\StreamedResponse;
use Illuminate\Support\Facades\Hash;
class UsersController class UsersController
{ {
/**
* 利用者分類選択肢を取得
*/
private function buildCategoryOptions(): array
{
return DB::table('usertype')
->orderBy('user_categoryid', 'asc')
->get()
->mapWithKeys(function ($row) {
$label = collect([
$row->usertype_subject1 ?? '',
$row->usertype_subject2 ?? '',
$row->usertype_subject3 ?? '',
])->filter(fn($v) => $v !== '')->implode('/');
return [$row->user_categoryid => $label !== '' ? $label : (string) $row->user_categoryid];
})
->toArray();
}
/**
* 利用者分類1(一覧絞り込み用)
*/
private function buildCategory1Options()
{
return DB::table('regular_contract')
->join('usertype', 'regular_contract.user_categoryid', '=', 'usertype.user_categoryid')
->whereNotNull('usertype.usertype_subject1')
->distinct()
->orderBy('usertype.usertype_subject1')
->pluck('usertype.usertype_subject1');
}
/**
* 利用者分類2(一覧絞り込み用)
*/
private function buildCategory2Options()
{
return DB::table('regular_contract')
->join('usertype', 'regular_contract.user_categoryid', '=', 'usertype.user_categoryid')
->whereNotNull('usertype.usertype_subject2')
->distinct()
->orderBy('usertype.usertype_subject2')
->pluck('usertype.usertype_subject2');
}
/**
* 利用者分類3(一覧絞り込み用)
*/
private function buildCategory3Options()
{
return DB::table('regular_contract')
->join('usertype', 'regular_contract.user_categoryid', '=', 'usertype.user_categoryid')
->whereNotNull('usertype.usertype_subject3')
->distinct()
->orderBy('usertype.usertype_subject3')
->pluck('usertype.usertype_subject3');
}
/** /**
* 利用者一覧 * 利用者一覧
* - テーブル名: user * - テーブル名: user
@ -99,15 +38,13 @@ class UsersController
'user_name', 'user_name',
'user_birthdate', 'user_birthdate',
'user_age', 'user_age',
'user_mobile',
'user_homephone', 'user_homephone',
'user_primemail', 'user_primemail',
'user_submail', 'user_submail',
'user_school', 'user_school',
]; ];
$sort = $request->input('sort', 'user_seq'); $sort = $request->input('sort', 'user_seq');
$dirParam = strtolower((string) $request->input('dir', $request->input('sort_type', 'asc'))); $sortType = strtolower($request->input('sort_type', 'desc')) === 'asc' ? 'asc' : 'desc';
$sortType = $dirParam === 'asc' ? 'asc' : 'desc';
if (!in_array($sort, $sortable, true)) { if (!in_array($sort, $sortable, true)) {
$sort = 'user_seq'; $sort = 'user_seq';
} }
@ -119,24 +56,14 @@ class UsersController
$user_phonetic = trim((string) $request->input('user_phonetic', '')); $user_phonetic = trim((string) $request->input('user_phonetic', ''));
$phone = trim((string) $request->input('phone', '')); // 携帯/自宅の両方対象 $phone = trim((string) $request->input('phone', '')); // 携帯/自宅の両方対象
$crime = trim((string) $request->input('crime', '')); // 防犯登録番号(暫定: qr_code $crime = trim((string) $request->input('crime', '')); // 防犯登録番号(暫定: qr_code
$email = trim((string) $request->input('email', '')); $user_categoryid = (string) $request->input('user_categoryid', '');
$user_category1 = trim((string) $request->input('user_category1', ''));
$user_category2 = trim((string) $request->input('user_category2', ''));
$user_category3 = trim((string) $request->input('user_category3', ''));
$tag_qr_flag = (string) $request->input('tag_qr_flag', ''); // 0=タグ / 1=QR $tag_qr_flag = (string) $request->input('tag_qr_flag', ''); // 0=タグ / 1=QR
$quit_flag = (string) $request->input('quit_flag', ''); // 0=いいえ / 1=はい $quit_flag = (string) $request->input('quit_flag', ''); // 0=いいえ / 1=はい
$quit_from = (string) $request->input('quit_from', ''); // YYYY-MM-DD $quit_from = (string) $request->input('quit_from', ''); // YYYY-MM-DD
$quit_to = (string) $request->input('quit_to', ''); // YYYY-MM-DD $quit_to = (string) $request->input('quit_to', ''); // YYYY-MM-DD
// ▼ ベースクエリ(一覧で使う列が多いので一旦 * を許容) // ▼ ベースクエリ(一覧で使う列が多いので一旦 * を許容)
$query = DB::table('user') $query = DB::table('user')->select('user.*');
->leftJoin('usertype', 'user.user_categoryid', '=', 'usertype.user_categoryid')
->select(
'user.*',
'usertype.usertype_subject1',
'usertype.usertype_subject2',
'usertype.usertype_subject3'
);
// ▼ テキスト系 // ▼ テキスト系
if ($user_id !== '') if ($user_id !== '')
@ -157,22 +84,10 @@ class UsersController
// ※ dump に防犯登録番号の明確なカラムが無いため暫定的に qr_code を対象 // ※ dump に防犯登録番号の明確なカラムが無いため暫定的に qr_code を対象
$query->where('user.qr_code', 'like', "%{$crime}%"); $query->where('user.qr_code', 'like', "%{$crime}%");
} }
if ($email !== '')
$query->where('user.user_primemail', 'like', "%{$email}%");
// ▼ セレクト/ラジオ('' 以外なら適用。'0' も通す) // ▼ セレクト/ラジオ('' 以外なら適用。'0' も通す)
if ($user_category1 !== '') { if ($user_categoryid !== '')
$query->where('usertype.usertype_subject1', $user_category1); $query->where('user.user_categoryid', $user_categoryid);
}
if ($user_category2 !== '') {
$query->where('usertype.usertype_subject2', $user_category2);
}
if ($user_category3 !== '') {
$query->where('usertype.usertype_subject3', $user_category3);
}
if ($tag_qr_flag !== '') if ($tag_qr_flag !== '')
$query->where('user.tag_qr_flag', (int) $tag_qr_flag); $query->where('user.tag_qr_flag', (int) $tag_qr_flag);
if ($quit_flag !== '') if ($quit_flag !== '')
@ -185,31 +100,24 @@ class UsersController
$query->where('user.user_quitday', '<=', $quit_to); $query->where('user.user_quitday', '<=', $quit_to);
// ▼ 並び & ページング // ▼ 並び & ページング
$list = $query->orderBy("user.{$sort}", $sortType)->paginate(50); $list = $query->orderBy("user.{$sort}", $sortType)->paginate(20);
// ▼ 画面に渡す(フォーム再描画用に絞り込み値も) // ▼ 画面に渡す(フォーム再描画用に絞り込み値も)
return view('admin.users.list', [ return view('admin.users.list', [
'list' => $list, 'list' => $list,
'sort' => $sort, 'sort' => $sort,
'sort_type' => $sortType, 'sort_type' => $sortType,
'dir' => $sortType,
'user_id' => $user_id, 'user_id' => $user_id,
'member_id' => $member_id, 'member_id' => $member_id,
'user_tag_serial' => $user_tag_serial, 'user_tag_serial' => $user_tag_serial,
'user_phonetic' => $user_phonetic, 'user_phonetic' => $user_phonetic,
'phone' => $phone, 'phone' => $phone,
'crime' => $crime, 'crime' => $crime,
'email' => $email, 'user_categoryid' => $user_categoryid,
'tag_qr_flag' => $tag_qr_flag, 'tag_qr_flag' => $tag_qr_flag,
'quit_flag' => $quit_flag, 'quit_flag' => $quit_flag,
'quit_from' => $quit_from, 'quit_from' => $quit_from,
'quit_to' => $quit_to, 'quit_to' => $quit_to,
'user_category1' => $user_category1,
'user_category2' => $user_category2,
'user_category3' => $user_category3,
'category1Options' => $this->buildCategory1Options(),
'category2Options' => $this->buildCategory2Options(),
'category3Options' => $this->buildCategory3Options(),
]); ]);
} }
@ -241,19 +149,9 @@ class UsersController
$q->where('user_primemail', 'like', "%{$v}%"); $q->where('user_primemail', 'like', "%{$v}%");
// ▼ セレクト/ラジオ('' だけスキップ。'0' は適用) // ▼ セレクト/ラジオ('' だけスキップ。'0' は適用)
$q->leftJoin('usertype', 'user.user_categoryid', '=', 'usertype.user_categoryid'); $val = (string) $request->input('user_categoryid', '');
if ($val !== '')
if (($v = trim((string) $request->input('user_category1', ''))) !== '') { $q->where('user_categoryid', $val);
$q->where('usertype.usertype_subject1', $v);
}
if (($v = trim((string) $request->input('user_category2', ''))) !== '') {
$q->where('usertype.usertype_subject2', $v);
}
if (($v = trim((string) $request->input('user_category3', ''))) !== '') {
$q->where('usertype.usertype_subject3', $v);
}
$val = (string) $request->input('tag_qr_flag', ''); $val = (string) $request->input('tag_qr_flag', '');
if ($val !== '') if ($val !== '')
@ -362,11 +260,11 @@ class UsersController
return view('admin.users.add'); return view('admin.users.add');
} }
// ▼ バリデーションuser_id は半角数字のみ) // ▼ バリデーションuser_id は半角数字のみ)
$rules = [ $rules = [
'user_id' => ['required', 'regex:/^\d+$/', 'digits_between:1,10'], // 半角数字最大10桁 'user_id' => ['required', 'regex:/^\d+$/'], // 半角数字のみ許可
'user_name' => ['required', 'string', 'max:255'], 'user_name' => ['required', 'string', 'max:255'],
'user_pass' => ['required', 'string', 'min:8', 'confirmed'],
// 任意 // 任意
'user_primemail' => ['nullable', 'email', 'max:255'], 'user_primemail' => ['nullable', 'email', 'max:255'],
'user_gender' => ['nullable', 'in:男性,女性'], 'user_gender' => ['nullable', 'in:男性,女性'],
@ -381,11 +279,6 @@ class UsersController
$messages = [ $messages = [
'user_id.required' => '利用者IDは必須です。', 'user_id.required' => '利用者IDは必須です。',
'user_id.regex' => '利用者IDは半角数字のみで入力してください。', 'user_id.regex' => '利用者IDは半角数字のみで入力してください。',
'user_id.digits_between' => '利用者IDは最大10桁以内で入力してください。',
'user_name.required' => '氏名は必須です。',
'user_pass.required' => 'パスワードは必須です。',
'user_pass.min' => 'パスワードは8文字以上で入力してください。',
'user_pass.confirmed' => 'パスワードと確認用パスワードが一致しません。',
]; ];
// ▼ 属性名(日本語ラベル) // ▼ 属性名(日本語ラベル)
@ -422,158 +315,4 @@ class UsersController
return redirect()->route('users')->with('success', '利用者を登録しました。'); return redirect()->route('users')->with('success', '利用者を登録しました。');
} }
/**
* 利用者編集GET: 表示 / POST: 更新)
*/
public function edit(Request $request, int $seq)
{
$user = DB::table('user')->where('user_seq', $seq)->first();
if (!$user) {
abort(404, '利用者情報が見つかりません。');
}
$operators = DB::table('ope')
->select('ope_id', 'ope_name')
->orderBy('ope_name')
->get();
$categoryOptions = $this->buildCategoryOptions();
// ▼ 退会処理専用hiddenフィールド quit_action があれば退会処理)
if ($request->has('quit_action')) {
DB::table('user')->where('user_seq', $seq)->update([
'user_quit_flag' => 1,
'user_quitday' => now()->format('Y-m-d'),
'ope_id' => $request->input('ope_id') ?? auth()->user()->ope_id ?? null,
'updated_at' => now(),
]);
return redirect()
->route('users_edit', ['seq' => $seq])
->with('status', '退会処理が完了しました。');
}
if ($request->isMethod('get')) {
return view('admin.users.edit', [
'user' => $user,
'operators' => $operators,
'categoryOptions' => $categoryOptions,
]);
}
$rules = [
'user_id' => ['required', 'regex:/^\d+$/', 'digits_between:1,10'],
'user_name' => ['required', 'string', 'max:255'],
'user_pass' => ['nullable', 'string', 'min:8', 'confirmed'],
'user_primemail' => ['nullable', 'email', 'max:255'],
'user_gender' => ['nullable', 'in:男性,女性,未入力'],
'member_id' => ['nullable', 'string', 'max:255'],
'user_mobile' => ['nullable', 'string', 'max:255'],
'user_homephone' => ['nullable', 'string', 'max:255'],
'user_birthdate' => ['nullable', 'date'],
'user_categoryid' => ['nullable', 'integer', 'exists:usertype,user_categoryid'],
'user_age' => ['nullable', 'integer', 'min:0'],
'user_chk_day' => ['nullable', 'date'],
'user_quitday' => ['nullable', 'date'],
'ope_id' => ['nullable', 'integer', 'exists:ope,ope_id'],
];
$messages = [
'user_id.required' => '利用者IDは必須です。',
'user_id.regex' => '利用者IDは半角数字のみで入力してください。',
'user_id.digits_between' => '利用者IDは最大10桁以内で入力してください。',
'user_name.required' => '氏名は必須です。',
];
$validator = Validator::make($request->all(), $rules, $messages);
if ($validator->fails()) {
return back()->withErrors($validator)->withInput();
}
$data = [
'user_id' => $request->input('user_id'),
'member_id' => $request->input('member_id'),
'user_name' => $request->input('user_name'),
'user_gender' => $request->input('user_gender'),
'user_mobile' => $request->input('user_mobile'),
'user_homephone' => $request->input('user_homephone'),
'user_birthdate' => $request->input('user_birthdate'),
'user_age' => $request->input('user_age'),
'user_categoryid' => $request->input('user_categoryid'),
'user_phonetic' => $request->input('user_phonetic'),
'user_tag_serial' => $request->input('user_tag_serial'),
'user_tag_serial_64' => $request->input('user_tag_serial_64'),
'qr_code' => $request->input('qr_code'),
'tag_qr_flag' => $request->input('tag_qr_flag', '0'),
'user_aid' => $request->input('user_aid'),
'user_place_qrid' => $request->input('user_place_qrid'),
'user_primemail' => $request->input('user_primemail'),
'user_submail' => $request->input('user_submail'),
'ward_residents' => $request->input('ward_residents'),
'user_workplace' => $request->input('user_workplace'),
'user_school' => $request->input('user_school'),
'user_graduate' => $request->input('user_graduate'),
'user_idcard' => $request->input('user_idcard'),
'user_idcard_chk_flag' => $request->input('user_idcard_chk_flag', '0'),
'user_chk_day' => $request->input('user_chk_day'),
'user_chk_opeid' => $request->input('ope_id'),
'ope_id' => $request->input('ope_id'),
'user_regident_zip' => $request->input('user_regident_zip'),
'user_regident_pre' => $request->input('user_regident_pre'),
'user_regident_city' => $request->input('user_regident_city'),
'user_regident_add' => $request->input('user_regident_add'),
'user_relate_zip' => $request->input('user_relate_zip'),
'user_relate_pre' => $request->input('user_relate_pre'),
'user_relate_city' => $request->input('user_relate_city'),
'user_relate_add' => $request->input('user_relate_add'),
'user_tag_issue' => $request->input('user_tag_issue'),
'issue_permission' => $request->input('issue_permission', '1'),
'user_quit_flag' => $request->input('user_quit_flag', '0'),
'user_quitday' => $request->input('user_quitday'),
'user_remarks' => $request->input('user_remarks'),
'updated_at' => now(),
];
if ($request->filled('user_pass')) {
$data['user_pass'] = Hash::make($request->input('user_pass'));
}
DB::table('user')->where('user_seq', $seq)->update($data);
return redirect()
->route('users_edit', ['seq' => $seq])
->with('status', '利用者情報を更新しました。');
}
/**
* 利用者削除POST: 削除実行)
* - user_seq をキーに削除処理を行う
* - 削除前に存在確認を行い、存在しない場合はエラーを返す
* - 削除完了後、一覧画面へリダイレクト
*/
public function delete(Request $request)
{
// ▼ パラメータ取得
$userSeq = (int) $request->input('user_seq');
// ▼ 対象レコード存在確認
$user = DB::table('user')->where('user_seq', $userSeq)->first();
if (!$user) {
// 該当データなし
return redirect()
->route('users')
->with('error', '利用者情報が見つかりません。');
}
// ▼ 削除処理実行
DB::table('user')->where('user_seq', $userSeq)->delete();
// ▼ 正常終了メッセージを一覧画面に表示
return redirect()
->route('users')
->with('success', '利用者を削除しました。');
}
} }

View File

@ -2,114 +2,290 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\UsertypeRequest; use App\Http\Requests\UsertypeRequest;
use App\Models\Usertype; use App\Models\Usertype;
use App\Services\UsertypeService; use App\Utils;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Validator;
use Response;
class UsertypeController extends Controller class UsertypeController extends Controller
{ {
public function __construct( public function list(Request $request)
private readonly UsertypeService $service
) {}
public function index(Request $request)
{ {
// sort の許可値チェック $inputs = [
$sort = $request->query('sort', 'user_categoryid'); 'isMethodPost' => 0,
$allowSort = [ 'isExport' => 0,
'user_categoryid', 'sort' => $request->input('sort', ''),
'sort_order', 'sort_type' => $request->input('sort_type', ''),
'usertype_subject1', 'page' => $request->get('page', 1),
'usertype_subject2',
'usertype_subject3',
'print_name',
]; ];
if (!in_array($sort, $allowSort, true)) { $inputs['isMethodPost'] = $request->isMethod('post');
$sort = 'user_categoryid'; $inputs['list'] = Usertype::search($inputs);
if ($inputs['list']->total() > 0 && $inputs['page'] > $inputs['list']->lastPage()) {
return redirect()->route('usertypes');
}
return view('admin.usertypes.list', $inputs);
} }
$sortType = strtolower($request->query('sort_type', 'asc')); public function add(Request $request)
if (!in_array($sortType, ['asc', 'desc'], true)) { {
$sortType = 'asc'; // 画面に戻すための初期値
$viewData = [
'sort_order' => old('sort_order', ''),
'category_name1' => old('category_name1', ''),
'category_name2' => old('category_name2', ''),
'category_name3' => old('category_name3', ''),
'print_name' => old('print_name', ''),
'usertype_money' => old('usertype_money', ''),
'usertype_remarks' => old('usertype_remarks', ''),
'isEdit' => 0,
'isInfo' => 0,
];
if ($request->isMethod('post')) {
// 入力値をまとめる
$inputs = [
'sort_order' => $request->input('sort_order'),
'category_name1' => $request->input('category_name1'),
'category_name2' => $request->input('category_name2'),
'category_name3' => $request->input('category_name3'),
'print_name' => $request->input('print_name'),
'usertype_money' => $request->input('usertype_money'),
'usertype_remarks' => $request->input('usertype_remarks'),
];
// バリデーションルール(最小限)
$rules = [
'sort_order' => 'nullable|integer',
'category_name1' => 'nullable|string|max:255',
'category_name2' => 'nullable|string|max:255',
'category_name3' => 'nullable|string|max:255',
'print_name' => 'required|string|max:255',
'usertype_money' => 'nullable|string|max:255',
'usertype_remarks' => 'nullable|string|max:255',
];
$messages = [
'print_name.required' => '印字名は必須です。',
'sort_order.integer' => 'ソートオーダーは数値で入力してください。',
];
$validator = Validator::make($inputs, $rules, $messages);
if ($validator->fails()) {
return back()->withErrors($validator)->withInput();
} }
$list = $this->service->paginateList( // 登録処理
$request->query('filter_sort_order'), $ok = false;
$request->query('filter_usertype_subject1'), \DB::transaction(function () use ($inputs, &$ok) {
$request->query('filter_usertype_subject2'), $new = new Usertype();
$request->query('filter_usertype_subject3'), $new->fill($inputs);
$sort, $ok = $new->save();
$sortType });
if ($ok) {
return redirect()->route('usertypes')->with('success', '登録しました。');
}
return back()->with('error', '登録に失敗しました。')->withInput();
}
// GET: 画面表示
return view('admin.usertypes.add', $viewData);
}
public function edit(Request $request, $id)
{
$usertype = Usertype::findOrFail($id);
// 画面に渡す初期データ(既存の構成に合わせて attributes を使う)
$data = $usertype->getAttributes();
if (method_exists($this, 'getDataDropList')) {
$dataList = $this->getDataDropList();
$data = array_merge($data, $dataList);
}
if ($request->isMethod('POST')) {
$type = false;
// ▼ 内蔵バリデーションFormRequest を使わない)
$rules = [
'sort_order' => 'nullable|integer',
'category_name1' => 'nullable|string|max:255',
'category_name2' => 'nullable|string|max:255',
'category_name3' => 'nullable|string|max:255',
'print_name' => 'required|string|max:255',
'usertype_money' => 'nullable|string|max:255',
'usertype_remarks' => 'nullable|string|max:255',
];
$messages = [
'print_name.required' => '印字名は必須です。',
'sort_order.integer' => 'ソートオーダーは数値で入力してください。',
];
$validator = Validator::make($request->all(), $rules, $messages);
// 入力値を $data にマージ(既存ロジック踏襲)
$requestAll = $request->all();
$data = array_merge($data, $requestAll);
if ($validator->fails()) {
return back()->withErrors($validator)->withInput();
}
\DB::transaction(function () use ($data, &$type, $usertype) {
// fill するフィールドだけを明示したい場合は only(...) で絞ってもOK
$usertype->fill([
'sort_order' => $data['sort_order'] ?? null,
'category_name1' => $data['category_name1'] ?? null,
'category_name2' => $data['category_name2'] ?? null,
'category_name3' => $data['category_name3'] ?? null,
'print_name' => $data['print_name'] ?? null,
'usertype_money' => $data['usertype_money'] ?? null,
'usertype_remarks' => $data['usertype_remarks'] ?? null,
]);
$usertype->save();
$type = true;
});
if ($type) {
return redirect()->route('usertypes')->with('success', '更新しました。');
}
return back()->with('error', '更新に失敗しました。')->withInput();
}
// GET: 画面表示(既存のビューに合わせて返す)
return view('admin.usertypes.edit', $data);
}
public function delete(Request $request)
{
// pk[] が無ければ ids[] を見る(両対応)
$arr_pk = $request->input('pk');
if (empty($arr_pk)) {
$arr_pk = $request->input('ids', []);
}
if (!is_array($arr_pk)) {
$arr_pk = [$arr_pk];
}
if (empty($arr_pk)) {
return redirect()->route('usertypes')->with('error', '削除するユーザーを選択してください。');
}
if (Usertype::deleteByPk($arr_pk)) {
return redirect()->route('usertypes')->with('success', '削除が完了しました。');
}
return redirect()->route('usertypes')->with('error', '削除に失敗しました。');
}
public function info(Request $request, $id)
{
return $this->edit($request, $id, 'admin.usertypes.info');
}
public function getDataDropList()
{
$data = [];
return $data;
}
public function export(Request $request)
{
$headers = array(
"Content-type" => "text/csv;charset=UTF-8",
'Content-Encoding: UTF-8',
"Content-Disposition" => "attachment; filename=file.csv",
"Pragma" => "no-cache",
"Cache-Control" => "must-revalidate, post-check=0, pre-check=0",
"Expires" => "0"
); );
$inputs = [
'isMethodPost' => 0,
'isExport' => 1,
'sort' => $request->input('sort', ''),
'sort_type' => $request->input('sort_type', ''),
return view('admin.usertypes.index', [ ];
'list' => $list,
'sort' => $sort, $dataExport = Usertype::search($inputs);
'sort_type' => $sortType, $columns = array(
'filter_sort_order' => $request->query('filter_sort_order', ''), __('利用者分類ID'),// 0
'filter_usertype_subject1' => $request->query('filter_usertype_subject1', ''), __('分類名'),// 1
'filter_usertype_subject2' => $request->query('filter_usertype_subject2', ''), __('適用料率'),// 2
'filter_usertype_subject3' => $request->query('filter_usertype_subject3', ''), __('備考'),// 3
]); );
$filename = "利用者分類マスタ.csv";
$file = fopen($filename, 'w+');
fputcsv($file, $columns);
foreach ($dataExport as $items) {
fputcsv(
$file,
array(
$items->user_categoryid,
$items->print_name,
$items->usertype_money,
$items->usertype_remarks, // 3
)
);
}
fclose($file);
return Response::download($filename, $filename, $headers);
} }
public function import(Request $request)
public function create()
{ {
return view('admin.usertypes.create', [ $file = $request->file('file');
'isEdit' => false, if (!empty($file)) {
]); $data = Utils::csvToArray($file);
$type = 1;
$msg = '';
$record = 0;
DB::beginTransaction();
try {
Usertype::query()->delete();
$col = 4;
foreach ($data as $key => $items) {
$record = $key + 2;
if (count($items) == $col) {
$row = new Usertype();
$row->user_categoryid = $items[0];
$row->print_name = $items[1];
$row->usertype_money = $items[2];
$row->usertype_remarks = $items[3];
if (!$row->save()) {
$type = 0;
$msg = '行:record型が一致しません。';
break;
}
} else {
$type = 0;
$msg = '行:record列数が一致しません。';
break;
}
}
} catch (\Exception $e) {
dd($e);
$msg = '行:record型が一致しません。';
$type = 0;
}
if ($type) {
DB::commit();
return redirect()->route('usertypes')->with('success', __('輸入成功'));
} else {
DB::rollBack();
return redirect()->route('usertypes')->with('error', __($msg, ['record' => $record]));
}
} else {
return redirect()->route('usertypes')->with('error', __('あなたはcsvファイルを選択していません。'));
}
} }
public function store(UsertypeRequest $request)
{
$this->service->create($request->validated());
return redirect()
->route('usertypes.index')
->with('success', '登録が完了しました。');
}
public function edit(int $id)
{
$record = Usertype::query()->findOrFail($id);
return view('admin.usertypes.edit', [
'record' => $record,
'isEdit' => true,
]);
}
public function update(UsertypeRequest $request, int $id)
{
$record = Usertype::query()->findOrFail($id);
$this->service->update($record, $request->validated());
return redirect()
->route('usertypes.index')
->with('success', '更新が完了しました。');
}
public function destroy(Request $request)
{
$ids = $request->input('pk', []);
if (!is_array($ids)) {
$ids = [$ids];
}
if ($ids === []) {
return redirect()
->route('usertypes.index')
->with('error', '削除対象を1件以上選択してください。');
}
Usertype::query()->whereIn('user_categoryid', $ids)->delete();
return redirect()
->route('usertypes.index')
->with('success', '削除が完了しました。');
}
} }

View File

@ -38,36 +38,76 @@ class UsingStatusController extends Controller
public function index(Request $request, UsingStatusService $service) public function index(Request $request, UsingStatusService $service)
{ {
try { try {
$parkId = $request->input('park_id'); // GET/POST どちらでも取得 // CSRF トークンの自動検証Laravel 12標準機能
// リクエストパラメータの取得
// Laravel 12変更点$request->input()の使用を推奨
$parkId = $request->input('park_id', null);
$isSearchRequest = $request->has('search') || $request->isMethod('post');
// ログ出力(デバッグ用)
Log::info('区画別利用率状況ページアクセス', [
'park_id' => $parkId,
'is_search' => $isSearchRequest,
'method' => $request->method()
]);
// 駐輪場一覧の取得(選択用ドロップダウン)
$parkList = $service->getParkList(); $parkList = $service->getParkList();
// 駐輪場が選択されている場合のみ取得。「全て/空」の場合は空コレクションを返す // 利用率統計データの取得
$utilizationStats = collect(); // Laravel 12変更点デフォルトで全データを表示ユーザー選択不要
if ($parkId !== null && $parkId !== '') {
$utilizationStats = $service->getUtilizationStats($parkId); $utilizationStats = $service->getUtilizationStats($parkId);
// データが空の場合の処理
if ($utilizationStats->isEmpty() && $parkId) {
// 指定された駐輪場のデータが見つからない場合
return redirect()->route('using_status')
->with('warning', '選択された駐輪場のデータが見つかりませんでした。');
} }
$totals = $service->calculateTotals($utilizationStats); // 検索要求でない場合は全データを表示
$hasData = $utilizationStats->isNotEmpty(); if (!$isSearchRequest && !$parkId) {
$isSearchRequest = ($request->isMethod('post') || $request->has('park_id')); $utilizationStats = $service->getUtilizationStats(null);
$selectedPark = $parkList->firstWhere('park_id', $parkId); }
// 合計値の計算
$totals = $service->calculateTotals($utilizationStats);
// 選択された駐輪場の情報
$selectedPark = null;
if ($parkId && $parkList->isNotEmpty()) {
$selectedPark = $parkList->firstWhere('park_id', $parkId);
}
// ビューに渡すデータの準備
$viewData = [
'parkList' => $parkList, // 駐輪場選択用リスト
'utilizationStats' => $utilizationStats, // 利用率統計データ
'totals' => $totals, // 合計値
'selectedParkId' => $parkId, // 選択された駐輪場ID
'selectedPark' => $selectedPark, // 選択された駐輪場情報
'isSearchRequest' => $isSearchRequest, // 検索リクエストかどうか
'hasData' => $utilizationStats->isNotEmpty() // データが存在するかどうか
];
// 成功メッセージの設定(検索時)
if ($isSearchRequest && $utilizationStats->isNotEmpty()) {
session()->flash('success', '利用率データを正常に取得しました。');
}
return view('admin.using_status.index', $viewData);
return view('admin.using_status.index', [
'parkList' => $parkList,
'utilizationStats' => $utilizationStats,
'totals' => $totals,
'selectedParkId' => $parkId,
'selectedPark' => $selectedPark,
'isSearchRequest' => $isSearchRequest,
'hasData' => $hasData,
]);
} catch (\Exception $e) { } catch (\Exception $e) {
// エラーログの出力
Log::error('区画別利用率状況ページエラー', [ Log::error('区画別利用率状況ページエラー', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'file' => $e->getFile(), 'file' => $e->getFile(),
'line' => $e->getLine(), 'line' => $e->getLine(),
'park_id' => $parkId ?? null, 'park_id' => $parkId ?? null
]); ]);
// エラー発生時のリダイレクト
return redirect()->route('using_status') return redirect()->route('using_status')
->with('error', 'データの取得中にエラーが発生しました。管理者にお問い合わせください。'); ->with('error', 'データの取得中にエラーが発生しました。管理者にお問い合わせください。');
} }
@ -160,7 +200,7 @@ class UsingStatusController extends Controller
foreach ($stats as $stat) { foreach ($stats as $stat) {
$rows[] = [ $rows[] = [
(string) $stat->park_name, (string) $stat->park_name,
(string) $stat->psection_subject, (string) $stat->ptype_subject,
(string) $stat->park_limit, (string) $stat->park_limit,
(string) $stat->current_count, (string) $stat->current_count,
(string) $stat->available, (string) $stat->available,

View File

@ -1,250 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Zone;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use App\Models\Park;
use App\Models\Ptype;
use App\Models\Psection;
class ZoneController extends Controller
{
/**
* 一覧表示(絞り込み対応)
*/
public function list(Request $request)
{
if ($request->input('action') === 'reset') {
return redirect()->route('zones');
}
// ソート設定
$sort = $request->input('sort', 'zone_id');
$sort_type = $request->input('sort_type', 'asc');
// ベースクエリ
$query = Zone::query();
// === 絞り込み条件 ===
if ($request->filled('zone_id')) {
$query->where('zone_id', $request->zone_id);
}
if ($request->filled('zone_name')) {
$query->where('zone_name', 'LIKE', "%{$request->zone_name}%");
}
if ($request->filled('park_id')) {
$query->where('park_id', $request->park_id);
}
if ($request->filled('ptype_id')) {
$query->where('ptype_id', $request->ptype_id);
}
if ($request->filled('psection_id')) {
$query->where('psection_id', $request->psection_id);
}
if ($request->has('use_flag') && $request->use_flag !== '') {
$query->where('use_flag', $request->use_flag);
}
// ページネーション
$zones = $query->orderBy($sort, $sort_type)->paginate(20);
// === 下拉选单用の一覧データ ===
$parkList = DB::table('park')->pluck('park_name', 'park_id');
$ptypeList = DB::table('ptype')->pluck('ptype_subject', 'ptype_id');
$psectionList = DB::table('psection')->pluck('psection_subject', 'psection_id');
return view('admin.zones.list', compact(
'zones', 'sort', 'sort_type',
'parkList', 'ptypeList', 'psectionList'
));
}
/**
* 新規登録(画面/処理)
*/
public function add(Request $request)
{
if ($request->isMethod('get')) {
$parkList = DB::table('park')->pluck('park_name', 'park_id');
$ptypeList = DB::table('ptype')->pluck('ptype_subject', 'ptype_id');
$psectionList = DB::table('psection')->pluck('psection_subject', 'psection_id');
return view('admin.zones.add', [
'isEdit' => false,
'record' => new Zone(),
'parkList' => $parkList,
'ptypeList' => $ptypeList,
'psectionList' => $psectionList,
]);
}
// ▼ POST時バリデーション
$rules = [
'park_id' => 'required|integer',
'ptype_id' => 'required|integer',
'psection_id' => 'required|integer',
'zone_name' => 'required|string|max:255',
'zone_number' => 'nullable|integer|min:0',
'zone_standard' => 'nullable|integer|min:0',
'zone_tolerance' => 'nullable|integer|min:0',
'zone_sort' => 'nullable|integer|min:0',
];
$messages = [
'park_id.required' => '駐輪場は必須です。',
'ptype_id.required' => '駐輪分類は必須です。',
'psection_id.required' => '車種区分は必須です。',
'zone_name.required' => 'ゾーン名は必須です。',
];
$validator = Validator::make($request->all(), $rules, $messages);
if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput();
}
// ▼ 登録処理
DB::transaction(function () use ($request) {
$new = new Zone();
$new->fill($request->only([
'park_id',
'ptype_id',
'psection_id',
'zone_name',
'zone_number',
'zone_standard',
'zone_tolerance',
'zone_sort',
]));
$new->save();
});
return redirect()->route('zones')->with('success', '登録しました。');
}
/**
* 編集(画面/処理)
*/
public function edit(Request $request, $id)
{
// 該当データ取得
$record = Zone::find($id);
if (!$record) {
abort(404);
}
$parkList = DB::table('park')->pluck('park_name', 'park_id');
$ptypeList = DB::table('ptype')->pluck('ptype_subject', 'ptype_id');
$psectionList = DB::table('psection')->pluck('psection_subject', 'psection_id');
if ($request->isMethod('get')) {
// 編集画面表示
return view('admin.zones.edit', [
'isEdit' => true,
'record' => $record,
'parkList' => $parkList,
'ptypeList' => $ptypeList,
'psectionList' => $psectionList,
]);
}
// ▼ POST時バリデーション
$rules = [
'park_id' => 'required|integer',
'ptype_id' => 'required|integer',
'psection_id' => 'required|integer',
'zone_name' => 'required|string|max:255',
'zone_number' => 'nullable|integer|min:0',
'zone_standard' => 'nullable|integer|min:0',
'zone_tolerance' => 'nullable|integer|min:0',
'zone_sort' => 'nullable|integer|min:0',
];
$messages = [
'park_id.required' => '駐輪場は必須です。',
'ptype_id.required' => '駐輪分類は必須です。',
'psection_id.required' => '車種区分は必須です。',
'zone_name.required' => 'ゾーン名は必須です。',
];
$validator = Validator::make($request->all(), $rules, $messages);
if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput();
}
// ▼ 更新処理
DB::transaction(function () use ($request, $record) {
$record->fill($request->only([
'park_id',
'ptype_id',
'psection_id',
'zone_name',
'zone_number',
'zone_standard',
'zone_tolerance',
'zone_sort',
]));
$record->save();
});
return redirect()->route('zones')->with('success', '更新しました。');
}
/**
* 削除(単一/複数対応)
*/
public function delete(Request $request)
{
$ids = [];
// 単一削除id 指定)
if ($request->filled('id')) {
$ids[] = (int) $request->input('id');
}
// 複数削除(チェックボックス pk[]
if (is_array($request->input('pk'))) {
$ids = array_merge($ids, $request->input('pk'));
}
// 重複除去 & 数値変換
$ids = array_values(array_unique(array_map('intval', $ids)));
// 削除対象がない場合
if (empty($ids)) {
return back()->with('error', '削除対象が選択されていません。');
}
// 削除実行
Zone::whereIn('zone_id', $ids)->delete();
return redirect()->route('zones')->with('success', '削除しました。');
}
/**
* バリデーション共通化
*/
private function validateZone(Request $request)
{
return $request->validate([
'zone_name' => 'required|string|max:50',
'park_id' => 'required|integer',
'ptype_id' => 'nullable|integer',
'psection_id' => 'nullable|integer',
'zone_number' => 'nullable|integer|min:0',
'zone_standard' => 'nullable|integer|min:0',
'zone_tolerance' => 'nullable|integer|min:0',
'use_flag' => 'nullable|boolean',
'memo' => 'nullable|string|max:255',
]);
}
}

View File

@ -1,138 +0,0 @@
<?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送信に失敗しました。もう一度お試しください。');
}
}
}

View File

@ -1,112 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
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
{
// パスワードリセット申請画面表示
public function showLinkRequestForm()
{
return view('auth.forgot-password');
}
// リセットメール送信
public function sendResetLinkEmail(Request $request)
{
$request->validate([
'email' => 'required|email',
'email_confirmation' => 'required|email|same:email',
], [
'email.required' => 'メールアドレスを入力してください。',
'email.email' => '正しいメールアドレス形式で入力してください。',
'email_confirmation.required' => '確認用メールアドレスを入力してください。',
'email_confirmation.email' => '正しいメールアドレス形式で入力してください。',
'email_confirmation.same' => 'メールアドレスが一致しません。',
]);
// ope_mailでユーザーを検索
$user = \App\Models\Ope::where('ope_mail', $request->input('email'))->first();
if (!$user) {
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' => $tokenHash,
'created_at' => now(),
]
);
// メール送信
try {
$resetUrl = url('/reset-password?token=' . $token . '&email=' . urlencode($user->ope_mail));
$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)
->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', 'パスワード再設定メールを送信しました。');
}
}

View File

@ -3,12 +3,8 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Mail\EmailOtpMail;
use App\Services\EmailOtpService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
@ -74,128 +70,60 @@ class LoginController extends Controller
/** /**
* ログインリクエストのバリデーション * ログインリクエストのバリデーション
* * Laravel 12変更点ope_id, ope_passフィールドを使用Laravel 5.7と同じ)
* 仕様上の入力名(フォーム側)は ope_id / ope_pass のまま維持し、
* 内部の認証キーは login_id に寄せるlogin_id 統一)。
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @return void * @return void
*/ */
protected function validateLogin(Request $request) protected function validateLogin(Request $request)
{ {
// 個別未入力メッセージ仕様1,2
$request->validate([ $request->validate([
'ope_id' => 'required|string', // フォームの入力名は現状維持(実体は login_id 'ope_id' => 'required|string', // オペレータID旧システムと同じ
'ope_pass' => 'required|string', 'ope_pass' => 'required|string', // オペレータパスワード(旧システムと同じ)
], [
'ope_id.required' => 'ログインIDが未入力です。',
'ope_pass.required' => 'パスワードが未入力です。',
]); ]);
} }
/** /**
* ログイン認証を試行 * ログイン認証を試行
* *
*
* - 画面入力ope_id= DBの login_id として扱う
* - 退職フラグチェックも login_id で取得して判定する
*
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @return bool * @return bool
*/ */
protected function attemptLogin(Request $request) protected function attemptLogin(Request $request)
{ {
// 先にIDのみでオペレータ取得して退職フラグを確認仕様5-1
$loginId = $request->input('ope_id'); // 入力名は ope_id だが中身は login_id
$operator = \App\Models\Ope::where('login_id', $loginId)->first();
if ($operator && (int)($operator->ope_quit_flag) === 1) {
// 退職扱いは認証失敗と同じメッセージ仕様5-1 と 3/4 統一表示)
return false;
}
// 認証実行credentials() で login_id / password を渡す)
return Auth::attempt($this->credentials($request), false); return Auth::attempt($this->credentials($request), false);
} }
/** /**
* 認証用の資格情報を取得 * 認証用の資格情報を取得
* * Laravel 12変更点ope_idとope_passをpasswordフィールドにマッピング
*
* - 認証IDを login_id に統一
* - パスワード入力ope_pass Auth 側の password にマッピング
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @return array * @return array
*/ */
protected function credentials(Request $request) protected function credentials(Request $request)
{ {
return [ // Laravel 5.7: ope_id, ope_passをそのまま使用
'login_id' => $request->input('ope_id'), // フォーム入力ope_id→ DB列 login_id // Laravel 12: ope_passをpasswordにマッピングして認証
'password' => $request->input('ope_pass'), // フォーム入力ope_pass→ 認証用 password return $request->only('ope_id') + ['password' => $request->input('ope_pass')];
];
} }
/** /**
* ログイン成功時のレスポンス * ログイン成功時のレスポンス
* *
* OTP認証チェック
* - 24時間以内に OTP 認証済みの場合:/home にリダイレクト
* - 未認証の場合OTP メール送信 /otp にリダイレクト
*
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
*/ */
protected function sendLoginResponse(Request $request) protected function sendLoginResponse(Request $request)
{ {
$request->session()->regenerate(); $request->session()->regenerate();
$this->clearLoginAttempts($request); $this->clearLoginAttempts($request);
// 仕様5: ログインIDをセッション保持
// ここで保持する値も 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); 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認証メールの送信に失敗しました。');
}
}
/** /**
* ログイン失敗時のレスポンス * ログイン失敗時のレスポンス
* *
@ -204,7 +132,6 @@ class LoginController extends Controller
*/ */
protected function sendFailedLoginResponse(Request $request) protected function sendFailedLoginResponse(Request $request)
{ {
// 画面側のエラー表示キーは仕様に合わせて ope_id のまま
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'ope_id' => [trans('auth.failed')], 'ope_id' => [trans('auth.failed')],
]); ]);
@ -238,8 +165,7 @@ class LoginController extends Controller
protected function hasTooManyLoginAttempts(Request $request) protected function hasTooManyLoginAttempts(Request $request)
{ {
return RateLimiter::tooManyAttempts( return RateLimiter::tooManyAttempts(
$this->throttleKey($request), $this->throttleKey($request), 5
5
); );
} }
@ -252,8 +178,7 @@ class LoginController extends Controller
protected function incrementLoginAttempts(Request $request) protected function incrementLoginAttempts(Request $request)
{ {
RateLimiter::hit( RateLimiter::hit(
$this->throttleKey($request), $this->throttleKey($request), 60
60
); );
} }
@ -282,16 +207,12 @@ class LoginController extends Controller
/** /**
* レート制限用のスロットルキーを取得 * レート制限用のスロットルキーを取得
* *
*
* - 画面入力名は ope_id のまま
* - ただし内容は login_id を想定ログインID文字列
*
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @return string * @return string
*/ */
protected function throttleKey(Request $request) protected function throttleKey(Request $request)
{ {
return Str::lower($request->input('ope_id')) . '|' . $request->ip(); return Str::lower($request->input('ope_id')).'|'.$request->ip();
} }
/** /**
@ -306,7 +227,6 @@ class LoginController extends Controller
$this->throttleKey($request) $this->throttleKey($request)
); );
// 画面側のエラー表示キーは仕様に合わせて ope_id のまま
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'ope_id' => [trans('auth.throttle', ['seconds' => $seconds])], 'ope_id' => [trans('auth.throttle', ['seconds' => $seconds])],
]); ]);

View File

@ -1,135 +0,0 @@
<?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;
}
}

View File

@ -1,96 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use App\Models\Ope;
class ResetPasswordController extends Controller
{
public function showResetForm(Request $request)
{
$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'));
}
public function reset(Request $request)
{
$request->validate([
'email' => 'required|email',
'token' => 'required',
'password' => 'required|confirmed|min:8',
]);
// トークンのハッシュ化
$tokenHash = hash('sha256', $request->token);
// トークン・メール・24時間以内の有効性をチェック
$record = DB::table('password_reset_tokens')
->where('ope_mail', $request->email)
->where('token', $tokenHash)
->first();
if (!$record) {
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時間が切れました。再度お手続きを行ってください。']);
}
// パスワード更新
$user = Ope::where('ope_mail', $request->email)->first();
if (!$user) {
return back()->withErrors(['email' => 'ユーザーが見つかりません。']);
}
$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('password.change.success');
}
}

View File

@ -3,8 +3,6 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Models\City;
use App\Services\MenuAccessService;
class HomeController extends Controller class HomeController extends Controller
{ {
@ -24,15 +22,10 @@ class HomeController extends Controller
* アプリケーションのダッシュボードを表示 * アプリケーションのダッシュボードを表示
* 認証後のホーム画面 * 認証後のホーム画面
* *
* @param MenuAccessService $menuAccessService メニューアクセス制御サービス
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function index(MenuAccessService $menuAccessService) public function index()
{ {
// ログイン中のオペレータが表示可能な自治体一覧を取得 return view('home');
$visibleCities = $menuAccessService->visibleCities();
$isSorin = $menuAccessService->isSorin();
return view('home', compact('visibleCities', 'isSorin'));
} }
} }

View File

@ -1,427 +0,0 @@
<?php
namespace App\Http\Controllers\Webhook;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
use App\Models\Batch\BatchLog;
use App\Models\SettlementTransaction;
use App\Models\RegularContract;
use App\Jobs\ProcessSettlementJob;
use Carbon\Carbon;
class WellnetController extends Controller
{
/**
* Wellnet PUSH 受信 (SHJ-4A)
* 受け取った SOAP/XML を解析し、settlement_transaction に登録。
* 幂等性チェック、データ検証、キュー投入を含む完全な処理を実行。
*/
public function receive(Request $request)
{
$startedAt = now();
$raw = $request->getContent();
$md5Hash = md5($raw);
// IP白名单检查如果配置了
if (!$this->validateClientIp($request->ip())) {
Log::warning('SHJ-4A IP白名单验证失败', [
'ip' => $request->ip(),
'content_length' => strlen($raw),
]);
return $this->errorResponse('Unauthorized IP', 403);
}
// 事前にログ記録(サイズ上限に注意)
Log::info('SHJ-4A Wellnet PUSH received', [
'length' => strlen($raw),
'content_type' => $request->header('Content-Type'),
'ip' => $request->ip(),
'md5_hash' => $md5Hash,
]);
// 共通バッチログ: start
$batch = BatchLog::createBatchLog(
'shj4a',
BatchLog::STATUS_START,
[
'ip' => $request->ip(),
'content_type' => $request->header('Content-Type'),
'content_length' => strlen($raw),
'md5_hash' => $md5Hash,
],
'SHJ-4A Wellnet PUSH start'
);
try {
// 【処理1】幂等性检查 - MD5重复检查
$existingByMd5 = SettlementTransaction::where('md5_string', $md5Hash)->first();
if ($existingByMd5) {
Log::info('SHJ-4A 幂等性: MD5重复检测', [
'md5_hash' => $md5Hash,
'existing_id' => $existingByMd5->settlement_transaction_id,
]);
$batch->update([
'status' => BatchLog::STATUS_SUCCESS,
'end_time' => now(),
'message' => 'SHJ-4A 幂等性: MD5重复直接返回成功',
'success_count' => 0, // 幂等返回不计入成功数
]);
return $this->successResponse('処理済み(幂等性)');
}
// 【処理2】SOAP/XML解析
$xml = @simplexml_load_string($raw);
if (!$xml) {
throw new \RuntimeException('Invalid XML/SOAP payload');
}
// Body 以下の最初の要素を取得
$nsBody = $xml->children('http://schemas.xmlsoap.org/soap/envelope/')->Body ?? null;
$payloadNode = $nsBody ? current($nsBody->children()) : $xml; // SOAPでなければ素のXML想定
// XML -> 配列化
$payloadArray = json_decode(json_encode($payloadNode), true) ?? [];
// 【処理3】データ抽出と正規化
$data = $this->extractSettlementData($payloadArray, $md5Hash);
// 【処理4】必須フィールド検証
$this->validateRequiredFields($data);
// 【処理5】複合キー重复检查contract_payment_number + pay_date + settlement_amount
$existingByComposite = $this->findExistingByCompositeKey($data);
if ($existingByComposite) {
Log::info('SHJ-4A 幂等性: 複合キー重复検出', [
'contract_payment_number' => $data['contract_payment_number'],
'pay_date' => $data['pay_date'],
'settlement_amount' => $data['settlement_amount'],
'existing_id' => $existingByComposite->settlement_transaction_id,
]);
$batch->update([
'status' => BatchLog::STATUS_SUCCESS,
'end_time' => now(),
'message' => 'SHJ-4A 幂等性: 複合キー重复,直接返回成功',
'success_count' => 0,
]);
return $this->successResponse('処理済み(幂等性)');
}
// 【処理6】データベース取込と関連処理
$settlementId = null;
DB::transaction(function() use ($data, $batch, &$settlementId) {
// 決済トランザクション登録
$settlement = SettlementTransaction::create($data);
$settlementId = $settlement->settlement_transaction_id;
// 契約テーブルの軽微な更新SHJ-4Bで正式更新
RegularContract::where('contract_payment_number', $data['contract_payment_number'])
->update(['contract_updated_at' => now()]);
// バッチログ成功更新
$batch->update([
'status' => BatchLog::STATUS_SUCCESS,
'end_time' => now(),
'message' => 'SHJ-4A Wellnet PUSH stored successfully',
'success_count' => 1,
'parameters' => json_encode([
'settlement_transaction_id' => $settlementId,
'contract_payment_number' => $data['contract_payment_number'],
'settlement_amount' => $data['settlement_amount'],
]),
]);
Log::info('SHJ-4A 決済トランザクション登録成功', [
'settlement_transaction_id' => $settlementId,
'contract_payment_number' => $data['contract_payment_number'],
'settlement_amount' => $data['settlement_amount'],
]);
});
// 【処理7】SHJ-4B用キュージョブ投入
try {
$jobContext = [
'contract_payment_number' => $data['contract_payment_number'],
'settlement_amount' => $data['settlement_amount'],
'pay_date' => $data['pay_date'],
'pay_code' => $data['pay_code'],
'triggered_by' => 'shj4a_webhook',
'triggered_at' => $startedAt->toISOString(),
];
ProcessSettlementJob::dispatch($settlementId, $jobContext);
Log::info('SHJ-4A ProcessSettlementJob投入成功', [
'settlement_transaction_id' => $settlementId,
'job_context' => $jobContext,
]);
} catch (\Throwable $jobError) {
// キュー投入失敗は警告レベル(メイン処理は成功済み)
Log::warning('SHJ-4A ProcessSettlementJob投入失敗', [
'settlement_transaction_id' => $settlementId,
'error' => $jobError->getMessage(),
'note' => '兜底巡検で処理される予定',
]);
}
return $this->successResponse();
} catch (\Throwable $e) {
Log::error('SHJ-4A error', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'md5_hash' => $md5Hash,
]);
if (isset($batch)) {
$batch->update([
'status' => BatchLog::STATUS_ERROR,
'end_time' => now(),
'message' => 'SHJ-4A failed: ' . $e->getMessage(),
'error_details' => $e->getTraceAsString(),
'error_count' => 1,
]);
}
return $this->errorResponse($e->getMessage());
}
}
/**
* IP白名单验证
*
* @param string $clientIp
* @return bool
*/
private function validateClientIp(string $clientIp): bool
{
$whitelist = config('services.wellnet.ip_whitelist', '');
if (empty($whitelist)) {
return true; // 白名单为空时不验证
}
$allowedIps = array_map('trim', explode(',', $whitelist));
foreach ($allowedIps as $allowedIp) {
if (strpos($allowedIp, '/') !== false) {
// CIDR記法対応
if ($this->ipInRange($clientIp, $allowedIp)) {
return true;
}
} else {
// 直接IP比較
if ($clientIp === $allowedIp) {
return true;
}
}
}
return false;
}
/**
* CIDR範囲でのIP检查
*
* @param string $ip
* @param string $range
* @return bool
*/
private function ipInRange(string $ip, string $range): bool
{
list($subnet, $bits) = explode('/', $range);
$ip = ip2long($ip);
$subnet = ip2long($subnet);
$mask = -1 << (32 - $bits);
$subnet &= $mask; # nb: in case the supplied subnet wasn't correctly aligned
return ($ip & $mask) == $subnet;
}
/**
* 決済データの抽出と正規化
*
* @param array $payloadArray
* @param string $md5Hash
* @return array
*/
private function extractSettlementData(array $payloadArray, string $md5Hash): array
{
// inData/Result系の取り出しキー名差異に寛容
$first = function(array $arr, array $keys, $default = null) {
foreach ($keys as $k) {
if (isset($arr[$k])) return is_array($arr[$k]) ? $arr[$k] : (string)$arr[$k];
}
return $default;
};
$flat = $payloadArray;
// よくある入れ子: { YoyakuNyukin: { inData: {...} } } / { YoyakuNyukinResponse: { YoyakuNyukinResult: {...} } }
foreach (['inData','YoyakuSyunoBarCodeResult','YoyakuNyukinResult','YoyakuSyunoETicketResult'] as $k) {
if (isset($flat[$k]) && is_array($flat[$k])) { $flat = $flat[$k]; }
}
$data = [
'pay_code' => $first($flat, ['NyukinPayCode','SyunoPayCode','BcPayCode']),
'contract_payment_number' => $first($flat, ['NyukinRecvNum','SyunoRecvNum','RecvNum','contract_payment_number']),
'corp_code' => $first($flat, ['NyukinCorpCode','SyunoCorpCode','BcCorpCode','CorpCode']),
'mms_date' => $first($flat, ['NyukinReferDate','SyunoMMSNo','MmsDate']),
'cvs_code' => $first($flat, ['NyukinCvsCode','CvsCode']),
'shop_code' => $first($flat, ['NyukinShopCode','ShopCode']),
'pay_date' => $first($flat, ['NyukinPaidDate','PaidDate']),
'settlement_amount' => $first($flat, ['NyukinPaidAmount','SyunoPayAmount','PaidAmount']),
'stamp_flag' => $first($flat, ['NyukinInshiFlag','InshiFlag']),
'status' => 'received',
'md5_string' => $md5Hash,
];
// データ正規化処理
$data = $this->normalizeSettlementData($data);
return $data;
}
/**
* 決済データの正規化
*
* @param array $data
* @return array
*/
private function normalizeSettlementData(array $data): array
{
// 金額を数値化(非負数)
if (!empty($data['settlement_amount'])) {
$amount = preg_replace('/[^\d.]/', '', $data['settlement_amount']);
$data['settlement_amount'] = max(0, (float)$amount);
} else {
$data['settlement_amount'] = null;
}
// 支払日時の正規化
if (!empty($data['pay_date'])) {
try {
$data['pay_date'] = Carbon::parse($data['pay_date'])->format('Y-m-d H:i:s');
} catch (\Throwable $e) {
Log::warning('SHJ-4A 支払日時解析失敗', [
'original_pay_date' => $data['pay_date'],
'error' => $e->getMessage(),
]);
$data['pay_date'] = null;
}
}
// 文字列フィールドのトリム
$stringFields = ['pay_code', 'contract_payment_number', 'corp_code', 'mms_date', 'cvs_code', 'shop_code', 'stamp_flag'];
foreach ($stringFields as $field) {
if (isset($data[$field])) {
$data[$field] = trim($data[$field]) ?: null;
}
}
return $data;
}
/**
* 必須フィールドの検証
*
* @param array $data
* @throws \RuntimeException
*/
private function validateRequiredFields(array $data): void
{
// 必須フィールドのチェック
if (empty($data['contract_payment_number'])) {
throw new \RuntimeException('必須フィールドが不足: contract_payment_number (RecvNum)');
}
if (!isset($data['settlement_amount']) || $data['settlement_amount'] === null) {
throw new \RuntimeException('必須フィールドが不足: settlement_amount');
}
if (empty($data['pay_date'])) {
throw new \RuntimeException('必須フィールドが不足: pay_date');
}
}
/**
* 複合キーによる既存レコード検索
*
* @param array $data
* @return SettlementTransaction|null
*/
private function findExistingByCompositeKey(array $data): ?SettlementTransaction
{
return SettlementTransaction::where('contract_payment_number', $data['contract_payment_number'])
->where('pay_date', $data['pay_date'])
->where('settlement_amount', $data['settlement_amount'])
->first();
}
/**
* 成功レスポンスの生成
*
* @param string $message
* @return \Illuminate\Http\Response
*/
private function successResponse(string $message = '正常処理'): \Illuminate\Http\Response
{
$responseFormat = config('services.wellnet.response_format', 'json');
if ($responseFormat === 'soap') {
return $this->soapResponse(0, $message);
} else {
return response()->json(['result' => 0, 'message' => $message]);
}
}
/**
* エラーレスポンスの生成
*
* @param string $message
* @param int $httpCode
* @return \Illuminate\Http\Response
*/
private function errorResponse(string $message, int $httpCode = 500): \Illuminate\Http\Response
{
$responseFormat = config('services.wellnet.response_format', 'json');
if ($responseFormat === 'soap') {
return $this->soapResponse(1, $message, $httpCode);
} else {
$resultCode = ($httpCode >= 500) ? 1 : 2; // サーバーエラー:1, クライアントエラー:2
return response()->json(['result' => $resultCode, 'error' => $message], $httpCode);
}
}
/**
* SOAP形式のレスポンス生成
*
* @param int $resultCode
* @param string $message
* @param int $httpCode
* @return \Illuminate\Http\Response
*/
private function soapResponse(int $resultCode, string $message, int $httpCode = 200): \Illuminate\Http\Response
{
$soapEnvelope = '<?xml version="1.0" encoding="utf-8"?>'
. '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">'
. '<soap:Body>'
. '<WellnetPushResponse>'
. '<Result>' . htmlspecialchars($resultCode) . '</Result>'
. '<Message>' . htmlspecialchars($message) . '</Message>'
. '</WellnetPushResponse>'
. '</soap:Body>'
. '</soap:Envelope>';
return response($soapEnvelope, $httpCode)
->header('Content-Type', 'text/xml; charset=utf-8');
}
}

View File

@ -1,34 +0,0 @@
<?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);
}
}

View File

@ -1,102 +0,0 @@
<?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;
}
}

View File

@ -1,57 +0,0 @@
<?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 認証が必要です。');
}
}

View File

@ -1,90 +0,0 @@
<?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);
}
}

View File

@ -1,97 +0,0 @@
<?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' => '新パスワードと新パスワード確認は一致する必要があります。',
];
}
}

View File

@ -1,59 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
final class CityRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
// 市区名:全角文字列 / 必須 / 1〜20文字
'city_name' => [
'required',
'string',
'min:1',
'max:20',
'regex:/^[^ -~。-゚]+$/u',
],
// 印字レイアウトファイル:半角英数字 / 必須 / 1〜255文字
'print_layout' => [
'required',
'string',
'min:1',
'max:255',
'regex:/^[A-Za-z0-9]+$/',
],
// 備考:任意文字列 / 最大255
'city_remarks' => [
'nullable',
'string',
'max:255',
],
];
}
public function messages(): array
{
return [
'city_name.required' => '市区名は必須です。',
'city_name.regex' => '市区名は全角文字で入力してください。',
'city_name.max' => '市区名は20文字以内で入力してください。',
'print_layout.required' => '印字レイアウトファイルは必須です。',
'print_layout.regex' => '印字レイアウトファイルは半角英数字で入力してください。',
'print_layout.max' => '印字レイアウトファイルは255文字以内で入力してください。',
'city_remarks.max' => '備考は255文字以内で入力してください。',
];
}
}

View File

@ -1,132 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ParkRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* add / edit 用的 validation
*/
public function rules(): array
{
if ($this->isMethod('post') || $this->isMethod('put')) {
return [
// 必須
'city_id' => ['required', 'integer'],
'park_name' => ['required', 'string', 'max:255'],
// 文字系
'park_ruby' => ['nullable', 'string', 'max:255'],
'park_syllabary' => ['nullable', 'string', 'max:3'],
'park_adrs' => ['nullable', 'string', 'max:255'],
'price_memo' => ['nullable', 'string', 'max:1000'],
// フラグ / 種別系integer想定
'park_close_flag' => ['nullable', 'integer'],
'inverse_use_flag1' => ['nullable', 'integer'], // 逆利用フラグ(一覧)
'inverse_use_flag2' => ['nullable', 'integer'], // 逆利用フラグ(学生)
'parking_regulations_flag' => ['nullable', 'integer'], // 駐輪規定フラグ
'alert_flag' => ['nullable', 'integer'], // 残警告チェックフラグ
'immediate_use_perm' => ['nullable', 'integer'], // 契約後即利用許可
'gender_display_flag' => ['nullable', 'integer'], // 項目表示設定:性別
'bd_display_flag' => ['nullable', 'integer'], // 項目表示設定:生年月日
'securityreg_display_flag' => ['nullable', 'integer'], // 項目表示設定:防犯登録番号
'park_fixed_contract' => ['nullable', 'integer'], // 駐輪場契約形態(定期)
'park_temporary_contract' => ['nullable', 'integer'], // 駐輪場契約形態(一時利用)
'park_available_time_flag' => ['nullable', 'integer'], // 利用可能時間制限フラグ
'park_manager_flag' => ['nullable', 'integer'], // 常駐管理人フラグ
'park_roof_flag' => ['nullable', 'integer'], // 屋根フラグ
'park_issuing_machine_flag' => ['nullable', 'integer'], // シール発行機フラグ
'reduction_guide_display_flag' => ['nullable', 'integer'], // 減免案内表示フラグ
'overyear_flag' => ['nullable', 'integer'], // 年跨ぎ
// 数値系
'print_number' => ['nullable', 'integer'], // 印字数
'distance_twopoints' => ['nullable', 'integer'], // 二点間距離
'reduction_age' => ['nullable', 'integer'], // 減免対象年齢
'reduction_guide_display_start_month' => ['nullable', 'integer'], // 減免案内表示開始月数
// 日付/時刻系date/time/datetime
'park_day' => ['nullable', 'date'], // 閉設日
'keep_alive' => ['nullable', 'date'], // 最新キープアライブ
'update_grace_period_start_date' => ['nullable', 'integer', 'between:1,31'], // 更新期間開始日
'update_grace_period_start_time' => ['nullable', 'date_format:H:i'], // 更新期間開始時
'update_grace_period_end_date' => ['nullable', 'integer', 'between:1,31'], // 更新期間終了日
'update_grace_period_end_time' => ['nullable', 'date_format:H:i'], // 更新期間終了時
'parking_start_grace_period' => ['nullable', 'integer', 'between:1,31'], // 駐輪開始猶予期間
'reminder_type' => ['nullable', 'integer'], // リマインダー種別
'reminder_time' => ['nullable', 'date_format:H:i'], // リマインダー時間
'park_available_time_from' => ['nullable', 'date_format:H:i'], // 利用可能時間(開始)
'park_available_time_to' => ['nullable', 'date_format:H:i'], // 利用可能時間(終了)
'park_manager_resident_from' => ['nullable', 'date_format:H:i'], // 常駐時間(開始)
'park_manager_resident_to' => ['nullable', 'date_format:H:i'], // 常駐時間(終了)
// 緯度/経度/電話など
'park_latitude' => ['nullable', 'string', 'max:50'], // 駐車場座標(緯度)
'park_longitude' => ['nullable', 'string', 'max:50'], // 駐車場座標(経度)
'park_tel' => ['nullable', 'string', 'max:50'], // 電話番号
// 備考/自由入力
'park_restriction' => ['nullable', 'string', 'max:50'], // 車種制限
'park_procedure' => ['nullable', 'string', 'max:50'], // 手続方法
'park_payment' => ['nullable', 'string', 'max:100'], // 支払方法
'park_using_method' => ['nullable', 'string', 'max:1000'], // 駐輪場利用方法
'park_contract_renewal_term' => ['nullable', 'string', 'max:255'], // 定期更新期間
'park_reservation' => ['nullable', 'string', 'max:255'], // 空き待ち予約
'park_reference' => ['nullable', 'string', 'max:1000'], // 特記事項
'student_id_confirmation_type' => ['nullable', 'string', 'max:255'], // 学生証確認種別
];
}
return [];
}
//
protected function prepareForValidation(): void
{
foreach ([
'park_available_time_from',
'park_available_time_to',
'park_manager_resident_from',
'park_manager_resident_to',
'update_grace_period_start_time',
'update_grace_period_end_time',
'reminder_time',
] as $key) {
if ($this->filled($key)) {
$v = (string) $this->input($key);
$this->merge([$key => substr($v, 0, 5)]); // HH:MM로 통일
}
}
}
/**
* add / edit payload
*/
public function payload(): array
{
return $this->validated();
}
public function filters(): array
{
return [
'park_name' => $this->input('park_name'),
'city_id' => $this->input('city_id'),
'sort' => $this->input('sort'),
'sort_type' => $this->input('sort_type', 'asc'),
];
}
}

View File

@ -1,27 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class RegularTypeRequest extends FormRequest
{
public function authorize()
{
return true; // 認証はコントローラーで行うため、ここでは常にtrueを返す
}
public function rules()
{
return [
'city_id' => 'required|string|max:255',
];
}
public function messages()
{
return [
'city_id.required' => '市区名は必須です。',
];
}
}

View File

@ -1,160 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
final class ReserveRequest extends FormRequest
{
public function rules(): array
{
$action = $this->routeAction();
if ($action === 'index') {
return [
'sort' => ['nullable', 'string'],
'sort_type' => ['nullable', 'in:asc,desc'],
'user_id' => ['nullable', 'string'],
'park_id' => ['nullable', 'integer'],
'reserve_date_from' => ['nullable', 'date'],
'reserve_date_to' => ['nullable', 'date'],
'keyword' => ['nullable', 'string'],
'valid_flag' => ['nullable', 'in:0,1'],
];
}
if ($action === 'destroy') {
return [
'ids' => ['required'],
];
}
if ($action === 'store') {
return [
'user_id' => ['required', 'integer'],
'park_id' => ['required', 'integer'],
'contract_id' => ['nullable', 'string'],
'price_parkplaceid' => ['nullable', 'integer'],
'psection_id' => ['nullable', 'integer'],
'reserve_date' => ['nullable', 'date'],
'valid_flag' => ['nullable', 'in:0,1'],
];
}
if ($action === 'update') {
return [
'user_id' => ['required', 'integer'],
'park_id' => ['required', 'integer'],
'contract_id' => ['nullable', 'string'],
'price_parkplaceid' => ['nullable', 'integer'],
'psection_id' => ['nullable', 'integer'],
'reserve_date' => ['nullable', 'date'],
'reserve_start' => ['nullable', 'date'],
'reserve_end' => ['nullable', 'date'],
'valid_flag' => ['nullable', 'in:0,1'],
'ope_id' => ['nullable', 'integer'],
];
}
// create/edit 등 “画面表示だけ” 는 검증 불필요
return [];
}
/**
* Why: Controller/Service 에서 공통으로 쓰는 입력 정규화.
*/
public function payload(): array
{
$action = $this->routeAction();
if ($action === 'index') {
return [
'sort' => (string) $this->input('sort', 'reserve_id'),
'sortType' => (string) $this->input('sort_type', 'asc'),
'userId' => trim((string) $this->input('user_id', '')),
'parkId' => $this->filled('park_id') ? (int) $this->input('park_id') : null,
'fromDt' => $this->input('reserve_date_from'),
'toDt' => $this->input('reserve_date_to'),
'keyword' => trim((string) $this->input('keyword', '')),
'validFlag' => $this->filled('valid_flag') ? (string) $this->input('valid_flag') : null,
];
}
if ($action === 'destroy') {
return [
'ids' => $this->normalizeIds($this->input('ids')),
];
}
if ($action === 'store' || $action === 'update') {
return [
'contractId' => $this->input('contract_id'),
'userCategoryId' => $this->filled('user_categoryid') ? (int) $this->input('user_categoryid') : null,
'contractCreatedAt' => $this->input('contract_created_at'),
'userId' => (int) $this->input('user_id'),
'parkId' => (int) $this->input('park_id'),
'priceParkplaceId' => $this->filled('price_parkplaceid') ? (int) $this->input('price_parkplaceid') : null,
'psectionId' => $this->filled('psection_id') ? (int) $this->input('psection_id') : null,
'ptypeId' => $this->filled('ptype_id') ? (int) $this->input('ptype_id') : null,
'reserveDate' => $this->input('reserve_date'),
'reserveStart' => $this->input('reserve_start'),
'reserveEnd' => $this->input('reserve_end'),
'reserveReduction' => $this->input('reserve_reduction'),
'reserveAutoRemind' => $this->input('reserve_auto_remind'),
'reserveManualRemind' => $this->input('reserve_manual_remind'),
'flag800m' => $this->filled('800m_flag') ? (int) $this->input('800m_flag') : null,
'reserveCancelday' => $this->input('reserve_cancelday'),
'validFlag' => $this->filled('valid_flag') ? (int) $this->input('valid_flag') : null,
'reserveManual' => $this->filled('reserve_manual') ? (int) $this->input('reserve_manual') : null,
'reserveNotice' => $this->input('reserve_notice'),
'sentDate' => $this->input('sent_date'),
'reserveOrder' => $this->filled('reserve_order') ? (int) $this->input('reserve_order') : null,
'reserveType' => $this->filled('reserve_type') ? (int) $this->input('reserve_type') : null,
'opeId' => $this->filled('ope_id') ? (int) $this->input('ope_id') : null,
];
}
return [];
}
private function routeAction(): string
{
// routes: reserves.index / reserves.store / reserves.update / reserves.destroy ...
$name = (string) $this->route()?->getName();
if ($name === '') {
return '';
}
$parts = explode('.', $name);
return (string) end($parts);
}
private function normalizeIds(mixed $raw): array
{
if (is_string($raw)) {
$raw = explode(',', $raw);
}
if (is_array($raw) && count($raw) === 1 && is_string($raw[0]) && str_contains($raw[0], ',')) {
$raw = explode(',', $raw[0]);
}
$ids = array_map('intval', (array) $raw);
$ids = array_values(array_unique(array_filter($ids, static fn (int $v): bool => $v > 0)));
return $ids;
}
}

View File

@ -1,91 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
final class UsertypeRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
// ソートオーダー:数値 / 必須
'sort_order' => [
'required',
'integer',
],
// 分類名1文字列 / 必須 / 最大10文字
'usertype_subject1' => [
'required',
'string',
'min:1',
'max:10',
],
// 分類名2文字列 / 必須 / 最大10文字
'usertype_subject2' => [
'required',
'string',
'min:1',
'max:10',
],
// 分類名3文字列 / 任意 / 最大10文字
'usertype_subject3' => [
'nullable',
'string',
'max:10',
],
// 印字名:文字列 / 任意 / 最大20文字
'print_name' => [
'nullable',
'string',
'max:20',
],
// 適用料率:文字列 / 任意 / 最大10文字
'usertype_money' => [
'nullable',
'string',
'max:10',
],
// 備考:文字列 / 任意 / 最大85文字
'usertype_remarks' => [
'nullable',
'string',
'max:85',
],
];
}
public function messages(): array
{
return [
'sort_order.required' => 'ソートオーダーは必須です。',
'sort_order.integer' => 'ソートオーダーは数値で入力してください。',
'usertype_subject1.required' => '分類名1は必須です。',
'usertype_subject1.max' => '分類名1は10文字以内で入力してください。',
'usertype_subject2.required' => '分類名2は必須です。',
'usertype_subject2.max' => '分類名2は10文字以内で入力してください。',
'usertype_subject3.max' => '分類名3は10文字以内で入力してください。',
'print_name.max' => '印字名は20文字以内で入力してください。',
'usertype_money.max' => '適用料率は10文字以内で入力してください。',
'usertype_remarks.max' => '備考は85文字以内で入力してください。',
];
}
}

View File

@ -1,183 +0,0 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use App\Models\Batch\BatchLog;
use App\Models\SettlementTransaction;
use App\Services\ShjFourBService;
/**
* SHJ-4B 決済トランザクション処理ジョブ
*
* SHJ-4Aで登録された決済情報を基に定期契約の更新処理を行う
*/
class ProcessSettlementJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* ジョブの実行可能回数
*
* @var int
*/
public $tries = 3;
/**
* ジョブの実行間隔(秒)
*
* @var array
*/
public $backoff = [60, 300, 900];
/**
* 使用するキュー名
*
* @var string
*/
public $queue = 'settlement';
/**
* 決済トランザクションID
*
* @var int
*/
protected $settlementTransactionId;
/**
* 追加のコンテキスト情報
*
* @var array
*/
protected $context;
/**
* コンストラクタ
*
* @param int $settlementTransactionId 決済トランザクションID
* @param array $context 追加のコンテキスト情報
*/
public function __construct(int $settlementTransactionId, array $context = [])
{
$this->settlementTransactionId = $settlementTransactionId;
$this->context = $context;
}
/**
* ジョブを実行
*
* SHJ-4Bサービスを使用して決済トランザクション処理を実行
*
* @return void
*/
public function handle()
{
$startTime = now();
// バッチログの開始記録
$batch = BatchLog::createBatchLog(
'shj4b',
BatchLog::STATUS_START,
[
'settlement_transaction_id' => $this->settlementTransactionId,
'context' => $this->context,
'job_id' => $this->job->getJobId(),
],
'SHJ-4B ProcessSettlementJob start'
);
try {
Log::info('SHJ-4B ProcessSettlementJob開始', [
'settlement_transaction_id' => $this->settlementTransactionId,
'context' => $this->context,
'start_time' => $startTime,
]);
// SHJ-4Bサービスを使用して決済トランザクション処理を実行
$shjFourBService = app(ShjFourBService::class);
$result = $shjFourBService->processSettlementTransaction(
$this->settlementTransactionId,
$this->context
);
// 処理結果に基づいてバッチログを更新
if ($result['success']) {
$batch->update([
'status' => BatchLog::STATUS_SUCCESS,
'end_time' => now(),
'message' => 'SHJ-4B ProcessSettlementJob completed successfully',
'success_count' => 1,
'parameters' => json_encode([
'result' => $result,
]),
]);
Log::info('SHJ-4B ProcessSettlementJob完了', [
'settlement_transaction_id' => $this->settlementTransactionId,
'execution_time' => now()->diffInSeconds($startTime),
'result' => $result,
]);
} else {
// ビジネスロジック上の問題(エラーではない)
$batch->update([
'status' => BatchLog::STATUS_SUCCESS,
'end_time' => now(),
'message' => 'SHJ-4B ProcessSettlementJob completed with issues: ' . $result['reason'],
'success_count' => 0,
'parameters' => json_encode([
'result' => $result,
'requires_manual_action' => true,
]),
]);
Log::warning('SHJ-4B ProcessSettlementJob要手動対応', [
'settlement_transaction_id' => $this->settlementTransactionId,
'result' => $result,
]);
}
} catch (\Throwable $e) {
Log::error('SHJ-4B ProcessSettlementJob失敗', [
'settlement_transaction_id' => $this->settlementTransactionId,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
// バッチログのエラー記録
$batch->update([
'status' => BatchLog::STATUS_ERROR,
'end_time' => now(),
'message' => 'SHJ-4B ProcessSettlementJob failed: ' . $e->getMessage(),
'error_details' => $e->getTraceAsString(),
'error_count' => 1,
]);
// ジョブを失敗させて再試行を促す
throw $e;
}
}
/**
* ジョブが失敗した場合の処理
*
* @param \Throwable $exception
* @return void
*/
public function failed(\Throwable $exception)
{
Log::error('SHJ-4B ProcessSettlementJob最終失敗', [
'settlement_transaction_id' => $this->settlementTransactionId,
'context' => $this->context,
'error' => $exception->getMessage(),
'attempts' => $this->attempts(),
]);
// 最終失敗時の追加処理があればここに記述
// 例:管理者への通知、障害キューへの登録など
}
}

View File

@ -66,6 +66,7 @@ class User extends Model
'user_remarks', 'user_remarks',
'user_age', 'user_age',
]; ];
protected static function boot() protected static function boot()
{ {
parent::boot(); parent::boot();

View File

@ -1,69 +0,0 @@
<?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 [];
}
}

View File

@ -7,16 +7,15 @@ use Illuminate\Database\Eloquent\Model;
class City extends Model class City extends Model
{ {
protected $table = 'city'; protected $table = 'city';
public $timestamps = true;
protected $primaryKey = 'city_id'; protected $primaryKey = 'city_id';
protected $keyType = 'int';
public $incrementing = true;
protected $fillable = [ protected $fillable = [
'city_id', 'city_id',
'city_name', 'city_name',
'print_layout', 'print_layout',
'city_user', 'city_user',
'city_remarks', 'city_remarks',
'management_id',
'created_at', 'created_at',
'updated_at', 'updated_at',
]; ];
@ -24,43 +23,12 @@ class City extends Model
/** /**
* 都市のリストを取得 * 都市のリストを取得
*/ */
public static function getList(?int $operatorId = null): array public static function getList()
{ {
return static::query() return self::all();
->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,
]);
}
} }

View File

@ -28,27 +28,20 @@ class ContractAllowableCity extends Model
*/ */
public static function search($inputs) public static function search($inputs)
{ {
$list = self::query() $list = self::query();
->leftJoin('park', 'contract_allowable_city.park_id', '=', 'park.park_id')
->leftJoin('city', 'contract_allowable_city.city_id', '=', 'city.city_id')
->select(
'contract_allowable_city.*',
'park.park_name',
'city.city_name'
);
if ($inputs['isMethodPost'] ?? false) { if ($inputs['isMethodPost'] ?? false) {
if (!empty($inputs['contract_allowable_city_id'])) { if (!empty($inputs['contract_allowable_city_id'])) {
$list->where('contract_allowable_city.contract_allowable_city_id', $inputs['contract_allowable_city_id']); $list->where('contract_allowable_city_id', $inputs['contract_allowable_city_id']);
} }
if (!empty($inputs['city_id'])) { if (!empty($inputs['city_id'])) {
$list->where('contract_allowable_city.city_id', $inputs['city_id']); $list->where('city_id', $inputs['city_id']);
} }
if (!empty($inputs['contract_allowable_city_name'])) { if (!empty($inputs['contract_allowable_city_name'])) {
$list->where('contract_allowable_city.contract_allowable_city_name', 'like', '%' . $inputs['contract_allowable_city_name'] . '%'); $list->where('contract_allowable_city_name', 'like', '%' . $inputs['contract_allowable_city_name'] . '%');
} }
if (!empty($inputs['park_id'])) { if (!empty($inputs['park_id'])) {
$list->where('contract_allowable_city.park_id', $inputs['park_id']); $list->where('park_id', $inputs['park_id']);
} }
} }
@ -64,7 +57,6 @@ class ContractAllowableCity extends Model
} }
} }
/** /**
* 主キーで取得 * 主キーで取得
*/ */

View File

@ -41,9 +41,4 @@ class Device extends Model
{ {
return static::orderBy('device_subject')->pluck('device_subject', 'device_id')->toArray(); return static::orderBy('device_subject')->pluck('device_subject', 'device_id')->toArray();
} }
public static function deleteByPk(array $ids): int
{
return static::whereIn('device_id', $ids)->delete();
}
} }

View File

@ -1,23 +0,0 @@
<?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', // 機能名
];
}

View File

@ -1,25 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class InvSetting extends Model
{
protected $table = 'inv_setting';
protected $primaryKey = 'seq';
public $timestamps = true;
protected $fillable = [
't_number', // 適格事業者番号
't_name', // 事業者名
'zipcode', // 郵便番号
'adrs', // 住所
'bldg', // 建物名
'tel_num', // 電話番号
'fax_num', // FAX番号
'company_image_path', // 会社ロゴ画像パス(任意)
];
}

View File

@ -4,23 +4,22 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class Station extends Model class NeighborStation extends Model
{ {
// テーブル名を指定
protected $table = 'station'; protected $table = 'station';
// 主キーを指定
protected $primaryKey = 'station_id'; protected $primaryKey = 'station_id';
// ホワイトリスト
protected $fillable = [ protected $fillable = [
'park_id', 'park_id',
'station_neighbor_station', 'station_neighbor_station',
'station_name_ruby', 'station_name_ruby',
'station_route_name', 'station_route_name',
'operator_id', 'operator_id',
'station_latitude', // ← 緯度
'station_longitude', // ← 経度
]; ];
public function park()
{
return $this->belongsTo(Park::class, 'park_id', 'park_id');
}
// タイムスタンプのカラム名がデフォルトと同じなので、特に設定不要
} }

View File

@ -19,7 +19,9 @@ class Ope extends Authenticatable
// オペレータタイプ定数(旧システムから継承) // オペレータタイプ定数(旧システムから継承)
const OPE_TYPE = [ const OPE_TYPE = [
'管理者', '管理者',
'職員', 'マネージャー',
'オペレーター',
'エリアマネージャー',
]; ];
protected $table = 'ope'; // データベーステーブル名(旧システムと同じ) protected $table = 'ope'; // データベーステーブル名(旧システムと同じ)
@ -27,42 +29,33 @@ class Ope extends Authenticatable
/** /**
* 一括代入可能な属性 * 一括代入可能な属性
* Laravel 5.7から引き継いだフィールド構成 + OTP認証フィールド * Laravel 5.7から引き継いだフィールド構成
*/ */
protected $fillable = [ protected $fillable = [
'//TODO オペレータID not found in database specs', '//TODO オペレータID not found in database specs',
'login_id', // ログインID
'password', // パスワード
'ope_name', // オペレータ名 'ope_name', // オペレータ名
'ope_type', // オペレータ種別 'ope_type', // オペレータ種別
'ope_mail', // メールアドレス(複数可) 'ope_mail', // メールアドレス
'ope_phone', // 電話番号 'ope_phone', // 電話番号
'ope_sendalart_que1', 'ope_sendalart_que1', // キュー1アラート送信
'ope_sendalart_que2', 'ope_sendalart_que2', // キュー2アラート送信
'ope_sendalart_que3', 'ope_sendalart_que3', // キュー3アラート送信
'ope_sendalart_que4', 'ope_sendalart_que4', // キュー4アラート送信
'ope_sendalart_que5', 'ope_sendalart_que5', // キュー5アラート送信
'ope_sendalart_que6', 'ope_sendalart_que6', // キュー6アラート送信
'ope_sendalart_que7', 'ope_sendalart_que7', // キュー7アラート送信
'ope_sendalart_que8', 'ope_sendalart_que8', // キュー8アラート送信
'ope_sendalart_que9', 'ope_sendalart_que9', // キュー9アラート送信
'ope_sendalart_que10', 'ope_sendalart_que10', // キュー10アラート送信
'ope_sendalart_que11', 'ope_sendalart_que11', // キュー11アラート送信
'ope_sendalart_que12', 'ope_sendalart_que12', // キュー12アラート送信
'ope_sendalart_que13', 'ope_sendalart_que13', // キュー13アラート送信
'ope_auth1', 'ope_auth1', // 権限1
'ope_auth2', 'ope_auth2', // 権限2
'ope_auth3', 'ope_auth3', // 権限3
'ope_auth4', 'ope_auth4', // 権限4
'ope_quit_flag', 'ope_quit_flag', // 退職フラグ
'ope_quitday', 'ope_quitday' // 退職日
// OTP認証関連フィールドメール二段階認証
'email_otp_code_hash',
'email_otp_expires_at',
'email_otp_last_sent_at',
'email_otp_verified_at',
// パスワード変更関連フィールド(定期パスワード変更)
'ope_pass_changed_at',
]; ];
/** /**
@ -131,7 +124,7 @@ class Ope extends Authenticatable
// POST検索条件の処理 // POST検索条件の処理
if ($inputs['isMethodPost']) { if ($inputs['isMethodPost']) {
// 検索条件があればここに追加
} }
// ソート処理 // ソート処理
@ -143,14 +136,12 @@ class Ope extends Authenticatable
if ($inputs['isExport']) { if ($inputs['isExport']) {
$list = $list->get(); $list = $list->get();
} else { } else {
// ページネーション件数を20に固定 $list = $list->paginate(\App\Utils::item_per_page);
$list = $list->paginate(20);
} }
return $list; return $list;
} }
/** /**
* プライマリキーでオペレータを取得 * プライマリキーでオペレータを取得
* *
@ -183,12 +174,4 @@ class Ope extends Authenticatable
{ {
return self::pluck('ope_name', 'ope_id'); return self::pluck('ope_name', 'ope_id');
} }
/**
* このオペレータが属する運営元を取得
*/
public function management(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Management::class, 'management_id', 'management_id');
}
} }

View File

@ -1,65 +0,0 @@
<?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();
}
}

View File

@ -1,25 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class OperatorLog extends Model
{
protected $table = 'operator_log';
protected $primaryKey = 'operator_log_id';
public $timestamps = false;
protected $fillable = [
'operator_id',
'remote_ip',
'browser_user_agent',
'user_id',
'contract_id',
'operation_code',
'operation_comment',
'operation_form_name',
'operation_table_name',
'created_at',
'updated_at',
];
}

View File

@ -28,16 +28,13 @@ class OperatorQue extends Model
1 => '本人確認(社会人)', 1 => '本人確認(社会人)',
2 => '本人確認(学生)', 2 => '本人確認(学生)',
3 => 'タグ発送', 3 => 'タグ発送',
4 => '予約告知通知', 4 => '予約告知電話',
5 => '定期更新通知', 5 => '定期更新電話',
6 => '返金処理', 6 => '返金',
7 => '再発行リミット超過', 7 => '再発行リミット超過',
8 => '支払い催促', 8 => '支払い催促',
9 => 'シール発行催促', 9 => 'シール発行催促',
10 => 'シール再発行', 10 => 'シール再発行許可',
11 => '名寄せフリガナ照合エラー',
12 => '本人確認(減免更新)',
13 => '本人確認(学生更新)',
101 => 'サーバーエラー', 101 => 'サーバーエラー',
102 => 'プリンタエラー', 102 => 'プリンタエラー',
103 => 'スキャナーエラー', 103 => 'スキャナーエラー',
@ -54,7 +51,7 @@ class OperatorQue extends Model
public function getQueClassLabel(): string public function getQueClassLabel(): string
{ {
return self::QueClass[$this->que_class] ?? 'キュー種別未設定'; return self::QueClass[$this->que_class] ?? (string)$this->que_class;
} }
public function getQueStatusLabel(): string public function getQueStatusLabel(): string

View File

@ -2,152 +2,86 @@
namespace App\Models; namespace App\Models;
use App\Utils;
use App\Models\City;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
class Park extends Model class Park extends Model
{ {
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
protected $table = 'park'; protected $table = 'park';
protected $primaryKey = 'park_id'; protected $primaryKey = 'park_id';
public $timestamps = true;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [ protected $fillable = [
'park_id', // 駐輪場ID 'park_name',
'city_id', // 市区 'park_address',
'park_name', // 駐輪場名 'park_phone',
'park_ruby', // 駐輪場ふりがな 'park_description',
'park_syllabary', // 駐輪場五十音 'park_status',
'park_adrs', // 住所 'park_capacity',
'park_close_flag', // 閉設フラグ 'park_price',
'park_day', // 閉設日 'park_operating_hours',
'price_memo', // 価格メモ
'alert_flag', // 残警告チェックフラグ
'print_number', // 印字数
'keep_alive', // 最新キープアライブ
'update_grace_period_start_date', // 更新期間開始日
'update_grace_period_start_time', // 更新期間開始時
'update_grace_period_end_date', // 更新期間終了日
'update_grace_period_end_time', // 更新期間終了時
'parking_start_grace_period', // 駐輪開始猶予期間
'reminder_type', // リマインダー種別
'reminder_time', // リマインダー時間
'immediate_use_permit', // 契約後即利用許可
'gender_display_flag', // 項目表示設定:性別
'bd_display_flag', // 項目表示設定:生年月日
'securityreg_display_flag', // 項目表示設定:防犯登録番号
'distance_twopoints', // 二点間距離
'park_latitude', // 駐車場座標(緯度)
'park_longitude', // 駐車場座標(経度)
'park_tel', // 電話番号
'park_fixed_contract', // 駐輪場契約形態(定期)
'park_temporary_contract', // 駐輪場契約形態(一時利用)
'park_restriction', // 車種制限
'park_procedure', // 手続方法
'park_payment', // 支払方法
'park_available_time_flag', // 利用可能時間制限フラグ
'park_available_time_from', // 利用可能時間(開始)
'park_available_time_to', // 利用可能時間(終了)
'park_manager_flag', // 常駐管理人フラグ
'park_manager_resident_from', // 常駐時間(開始)
'park_manager_resident_to', // 常駐時間(終了)
'park_roof_flag', // 屋根フラグ
'park_issuing_machine_flag', // シール発行機フラグ
'park_using_method', // 駐輪場利用方法
'park_contract_renewal_term', // 定期更新期間
'park_reservation', // 空き待ち予約
'park_reference', // 特記事項
'student_id_confirmation_type', // 学生証確認種別
'reduction_guide_display_flag', // 減免案内表示フラグ
'reduction_age', // 減免対象年齢
'reduction_guide_display_start_month', // 減免案内表示開始月数
'overyear_flag', // 年跨ぎ
'inverse_use_flag1', // 逆利用一般
'inverse_use_flag2', // 逆利用学生
'parking_regulations_flag' // 駐輪規定フラグ
]; ];
/**
* 駐車場検索
*/
public static function search($inputs) public static function search($inputs)
{ {
$list = self::query(); $query = self::query();
if ($inputs['isMethodPost']) {
// 検索条件の適用
if (!empty($inputs['park_name'])) {
$query->where('park_name', 'like', '%' . $inputs['park_name'] . '%');
} }
// Sort if (!empty($inputs['park_address'])) {
if ($inputs['sort']) { $query->where('park_address', 'like', '%' . $inputs['park_address'] . '%');
$list->orderBy($inputs['sort'], $inputs['sort_type']);
} }
if ($inputs['isExport']){ if (isset($inputs['park_status']) && $inputs['park_status'] !== '') {
$list = $list->get(); $query->where('park_status', $inputs['park_status']);
}else{
$list = $list->paginate(Utils::item_per_page);
}
return $list;
} }
public static function getByPk($pk) // ソート
{ if (!empty($inputs['sort'])) {
return self::find($pk); $sortType = !empty($inputs['sort_type']) ? $inputs['sort_type'] : 'asc';
$query->orderBy($inputs['sort'], $sortType);
} else {
$query->orderBy('park_id', 'desc');
} }
public static function deleteByPk($arr) // エクスポート用の場合はページネーションしない
{ if (!empty($inputs['isExport'])) {
return self::whereIn('park_id', $arr)->delete(); return $query->get();
} }
public static function boot() // ページネーションUtilsクラスの定数を使用
{ return $query->paginate(\App\Utils::item_per_page);
parent::boot();
self::creating(function (Park $model) {
$model->operator_id = Auth::user()->ope_id ?? null;
});
} }
/** /**
* GET 閉設フラグ * IDで駐車場を取得
*/ */
public function getParkCloseFlagDisplay() { public static function getParkById($id)
if($this->park_close_flag == 1) {
return '閉設';
}
else if($this->park_close_flag == 0) {
return '開設';
}
return '';
}
public function getCity()
{ {
// city_id => city_id (City モデル要有 city_id PK) return self::find($id);
return $this->belongsTo(City::class, 'city_id', 'city_id')->first();
}
public static function getList(){
return self::pluck('park_name','park_id');
}
public static function getIdByName($park_name){
return self::where('park_name',$park_name)->pluck('park_id')->first();
} }
/** /**
* 料金設定との関連付け * 駐車場リストを取得(ドロップダウン用)
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/ */
public function prices() public static function getList()
{ {
return $this->hasMany(PriceA::class, 'park_id', 'park_id'); return self::pluck('park_name', 'park_id')->toArray();
} }
/**
* 定期契約とのリレーション
*/
public function regularContracts()
{
return $this->hasMany(RegularContract::class, 'park_id', 'park_id');
}
} }

View File

@ -1,30 +0,0 @@
<?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 がある場合)
}

View File

@ -1,32 +0,0 @@
<?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;
}
}

View File

@ -65,14 +65,10 @@ class Pplace extends Model
/** /**
* 主キー配列で一括削除 * 主キー配列で一括削除
*/ */
public static function deleteByPk($ids) public static function deleteByPk($arr)
{ {
if (!is_array($ids)) { return self::whereIn('pplace_id', $arr)->delete();
$ids = [$ids];
} }
return self::whereIn('pplace_id', $ids)->delete();
}
/** /**
* 選択リスト取得用(フォーム等) * 選択リスト取得用(フォーム等)

View File

@ -17,11 +17,10 @@ class Price extends Model
5 => '12ヶ月', 5 => '12ヶ月',
]; ];
protected $table = 'price'; protected $table = 'price_a';
protected $primaryKey = 'price_parkplaceid'; protected $primaryKey = 'price_parkplaceid';
protected $fillable = [ protected $fillable = [
'price_parkplaceid',
'prine_name', 'prine_name',
'price_month', 'price_month',
'park_id', 'park_id',
@ -42,46 +41,34 @@ class Price extends Model
public static function search($inputs) public static function search($inputs)
{ {
$query = self::query() $list = self::query();
->select( // 只有在sort是有效字段时才排序
'price.*',
\DB::raw("CONCAT_WS('', usertype.usertype_subject1, usertype.usertype_subject2, usertype.usertype_subject3) as user_category_name"),
'psection.psection_subject',
'ptype.ptype_subject'
)
->leftJoin('usertype', 'price.user_categoryid', '=', 'usertype.user_categoryid')
->leftJoin('psection', 'price.psection_id', '=', 'psection.psection_id')
->leftJoin('ptype', 'price.price_ptypeid', '=', 'ptype.ptype_id');
// ソート対象カラム
$allowedSortColumns = [ $allowedSortColumns = [
'price_parkplaceid', // 駐車場所ID 'price_parkplaceid',
'park_id', // 駐輪場ID 'prine_name',
'prine_name', // 商品名 'price_month',
'price_month', // 期間 'park_id',
'user_categoryid', // 利用者分類ID 'psection_id',
'price', // 駐輪料金(税込) 'price_ptypeid',
'psection_id', // 車種区分ID 'user_categoryid',
'price_ptypeid', // 駐輪分類ID 'pplace_id',
'pplace_id', // 駐車車室ID 'price'
]; ];
$sort_column = $inputs['sort'] ?? '';
$sortColumn = $inputs['sort'] ?? ''; $sort_type = strtolower($inputs['sort_type'] ?? 'asc');
$sortType = strtolower($inputs['sort_type'] ?? 'asc'); if (in_array($sort_column, $allowedSortColumns)) {
if (!in_array($sort_type, ['asc', 'desc'])) {
if (in_array($sortColumn, $allowedSortColumns, true)) { $sort_type = 'asc';
if (!in_array($sortType, ['asc', 'desc'], true)) {
$sortType = 'asc';
} }
$query->orderBy($sortColumn, $sortType); $list->orderBy($sort_column, $sort_type);
} }
if ($inputs['isExport']) {
return $inputs['isExport'] $list = $list->get();
? $query->get() } else {
: $query->paginate(\App\Utils::item_per_page ?? 20); $list = $list->paginate(Utils::item_per_page);
}
return $list;
} }
public static function getByPk($pk) public static function getByPk($pk)
@ -115,26 +102,4 @@ class Price extends Model
return $this->belongsTo(Usertype::class, 'user_categoryid', 'user_categoryid')->first(); return $this->belongsTo(Usertype::class, 'user_categoryid', 'user_categoryid')->first();
} }
public function psection()
{
return $this->belongsTo(Psection::class, 'psection_id'); // 外部キーが psection_id
}
public function ptype()
{
return $this->belongsTo(Ptype::class, 'price_ptypeid'); // 外部キーが price_ptypeid
}
public function pplace()
{
return $this->belongsTo(Pplace::class, 'pplace_id'); // 外部キーが pplace_id
}
// public function getStation()
// {
// return $this->belongsTo(Station::class, 'price_parkplaceid', 'park_id');
// }
} }

View File

@ -4,24 +4,12 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
/**
* 車種分類モデル - ptypeテーブル正式モデル
* 旧UsingStatusPtypeの責務を置き換え
*/
class Ptype extends Model class Ptype extends Model
{ {
/**
* 主キー配列で一括削除
*/
public static function deleteByPk($arr)
{
return self::whereIn('ptype_id', $arr)->delete();
}
/**
* 主キーで1件取得
*/
public static function getByPk($pk)
{
return self::find($pk);
}
protected $table = 'ptype'; protected $table = 'ptype';
protected $primaryKey = 'ptype_id'; protected $primaryKey = 'ptype_id';
public $timestamps = true; public $timestamps = true;
@ -33,31 +21,11 @@ class Ptype extends Model
'ptype_subject', 'ptype_subject',
'ptype_remarks', 'ptype_remarks',
'operator_id', 'operator_id',
'floor_sort',
]; ];
public static function search($inputs) /**
{ * 料金設定
$list = self::query(); */
if (!empty($inputs['isMethodPost'])) {
// 必要に応じて検索条件を追加
}
if (!empty($inputs['sort'])) {
$list->orderBy($inputs['sort'], $inputs['sort_type'] ?? 'asc');
}
if (!empty($inputs['isExport'])) {
return $list->get();
} else {
return $list->paginate(20);
}
}
public static function getList()
{
return self::pluck('ptype_subject', 'ptype_id');
}
public function prices() public function prices()
{ {
return $this->hasMany(PriceA::class, 'price_ptypeid', 'ptype_id'); return $this->hasMany(PriceA::class, 'price_ptypeid', 'ptype_id');

View File

@ -1,48 +0,0 @@
<?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;
}
});
}
}

View File

@ -29,7 +29,6 @@ class RegularType extends Model
'regular_class_6', 'regular_class_6',
'regular_class_12', 'regular_class_12',
'memo', 'memo',
'operator_id',
]; ];
public static function boot() public static function boot()
@ -61,14 +60,14 @@ class RegularType extends Model
return $list; return $list;
} }
public static function getById($id) public static function getByPk($pk)
{ {
return self::find($id); return self::find($pk);
} }
public static function deleteById($id) public static function deleteByPk($arr)
{ {
return self::where('regular_type_id', $id)->delete(); return self::whereIn('regular_type_id', $arr)->delete();
} }
public function getCity() public function getCity()

View File

@ -24,7 +24,7 @@ class Setting extends Model
'web_master', // ウェブ参照マスタ 'web_master', // ウェブ参照マスタ
'auto_change_date', // ウェブ参照マスタ自動切り替え日時 'auto_change_date', // ウェブ参照マスタ自動切り替え日時
'auto_chage_master', // 自動切換えウェブ参照マスタ※DB定義のままchage 'auto_chage_master', // 自動切換えウェブ参照マスタ※DB定義のままchage
're-issue_alert_number', // 再発行アラート回数 're_issue_alert_number', // 再発行アラート回数
'image_base_url1', // ニュースイメージURLベース名 'image_base_url1', // ニュースイメージURLベース名
'image_base_url2', // 本人確認写真URLベース名 'image_base_url2', // 本人確認写真URLベース名
'printable_alert_flag', // プリンタ印字残警告フラグ 'printable_alert_flag', // プリンタ印字残警告フラグ
@ -37,7 +37,7 @@ class Setting extends Model
// キャスト(型変換) // キャスト(型変換)
protected $casts = [ protected $casts = [
'auto_change_date' => 'datetime', // 日時 'auto_change_date' => 'datetime', // 日時
're-issue_alert_number' => 'integer', // 整数 're_issue_alert_number' => 'integer', // 整数
'printable_alert_flag' => 'boolean', // 真偽値 'printable_alert_flag' => 'boolean', // 真偽値
'printable_number' => 'integer', // 整数 'printable_number' => 'integer', // 整数
'printable_alert_number' => 'integer', // 整数 'printable_alert_number' => 'integer', // 整数
@ -46,17 +46,4 @@ class Setting extends Model
'created_at' => 'datetime', // 作成日時 'created_at' => 'datetime', // 作成日時
'updated_at' => 'datetime', // 更新日時 'updated_at' => 'datetime', // 更新日時
]; ];
// アクセサgetter
public function getReissueAlertNumberAttribute()
{
return $this->attributes['re-issue_alert_number'] ?? null;
}
// ミューテタsetter
public function setReissueAlertNumberAttribute($value)
{
$this->attributes['re-issue_alert_number'] = $value;
}
} }

View File

@ -24,11 +24,4 @@ class SettlementTransaction extends Model
'stamp_flag', 'stamp_flag',
'md5_string', 'md5_string',
]; ];
// 日付型キャスト
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
'pay_date' => 'datetime',
];
} }

View File

@ -34,11 +34,4 @@ class Term extends Model
{ {
return self::all(); return self::all();
} }
public static function deleteByPk($ids)
{
if (!is_array($ids)) {
$ids = [$ids];
}
return self::whereIn('terms_id', $ids)->delete();
}
} }

View File

@ -2,7 +2,6 @@
namespace App\Models; namespace App\Models;
use App\Utils;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@ -16,14 +15,9 @@ class Usertype extends Model
protected $primaryKey = 'user_categoryid'; protected $primaryKey = 'user_categoryid';
protected $fillable = [ protected $fillable = [
'sort_order',
'usertype_subject1',
'usertype_subject2',
'usertype_subject3',
'print_name', 'print_name',
'usertype_money', 'usertype_money',
'usertype_remarks', 'usertype_remarks'
'operator_id',
]; ];
public static function boot() public static function boot()
@ -34,47 +28,22 @@ class Usertype extends Model
}); });
} }
public static function search(array $inputs) public static function search($inputs)
{ {
$query = self::query(); $list = self::query();
$table = (new self())->getTable(); if ($inputs['isMethodPost']) {
if (!empty($inputs['filter_sort_order'])) {
$query->where('sort_order', $inputs['filter_sort_order']);
} }
if (!empty($inputs['filter_usertype_subject1'])) { // Sort
$query->where('usertype_subject1', 'like', '%' . $inputs['filter_usertype_subject1'] . '%'); if ($inputs['sort']) {
$list->orderBy($inputs['sort'], $inputs['sort_type']);
} }
if (!empty($inputs['filter_usertype_subject2'])) { if ($inputs['isExport']){
$query->where('usertype_subject2', 'like', '%' . $inputs['filter_usertype_subject2'] . '%'); $list = $list->get();
}else{
$list = $list->paginate(Utils::item_per_page);
} }
if (!empty($inputs['filter_usertype_subject3'])) { return $list;
$query->where('usertype_subject3', 'like', '%' . $inputs['filter_usertype_subject3'] . '%');
}
$sortable = [
'user_categoryid' => "{$table}.user_categoryid",
'sort_order' => "{$table}.sort_order",
'usertype_subject1' => "{$table}.usertype_subject1",
'usertype_subject2' => "{$table}.usertype_subject2",
'usertype_subject3' => "{$table}.usertype_subject3",
'print_name' => "{$table}.print_name",
'usertype_remarks' => "{$table}.usertype_remarks",
];
$sortKey = $inputs['sort'] ?? 'user_categoryid';
$sortColumn = $sortable[$sortKey] ?? "{$table}.user_categoryid";
$direction = strtolower($inputs['sort_type'] ?? 'asc');
if (!in_array($direction, ['asc', 'desc'], true)) {
$direction = 'asc';
}
$query->orderBy($sortColumn, $direction);
return !empty($inputs['isExport'])
? $query->get()
: $query->paginate(Utils::item_per_page);
} }
public static function getByPk($pk) public static function getByPk($pk)

View File

@ -1,60 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Zone extends Model
{
use HasFactory;
/**
* テーブル名
*/
protected $table = 'zone';
/**
* 主キー
*/
protected $primaryKey = 'zone_id';
/**
* タイムスタンプを有効化
*/
public $timestamps = true;
/**
* 一括代入可能なカラム
*/
protected $fillable = [
'park_id',
'park_name',
'ptype_id',
'psection_id',
'zone_name',
'zone_number',
'zone_standard',
'zone_tolerance',
'zone_sort',
'delete_flag',
'ope_id',
'created_at',
'updated_at',
];
public function park()
{
return $this->belongsTo(Park::class, 'park_id', 'park_id');
}
public function ptype()
{
return $this->belongsTo(Ptype::class, 'ptype_id', 'ptype_id');
}
public function psection()
{
return $this->belongsTo(Psection::class, 'psection_id', 'psection_id');
}
}

View File

@ -1,62 +0,0 @@
<?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');
}
}

View File

@ -8,8 +8,6 @@ use App\Services\ShjMailSendService;
use App\Services\ShjNineService; use App\Services\ShjNineService;
use App\Services\ShjTenService; use App\Services\ShjTenService;
use App\Services\ShjSixService; use App\Services\ShjSixService;
use Illuminate\Support\Facades\DB;
use App\Services\MenuAccessService;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
@ -77,49 +75,6 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function boot(): void public function boot(): void
{ {
view()->composer('layouts.app', function($view){ //
// 未対応のみ集計
$stats = DB::table('operator_que')
->selectRaw("
SUM(CASE WHEN que_status = 1 AND que_class < 100 THEN 1 ELSE 0 END) AS task_untreated,
MAX(CASE WHEN que_status = 1 AND que_class < 100 THEN created_at END) AS task_latest,
SUM(CASE WHEN que_status = 1 AND que_class > 99 THEN 1 ELSE 0 END) AS hard_untreated,
MAX(CASE WHEN que_status = 1 AND que_class > 99 THEN created_at END) AS hard_latest
")
->first();
// 変数名は互換維持(内容は未対応件数)
$taskCount = (int)($stats->task_untreated ?? 0);
$hardCount = (int)($stats->hard_untreated ?? 0);
$taskLatest = $stats->task_latest ?? null;
$hardLatest = $stats->hard_latest ?? null;
// ドロップダウン最新5件 も未対応のみ
$latestTasks = DB::table('operator_que')
->where('que_status',1)
->where('que_class','<',100)
->orderByDesc('created_at')
->limit(5)
->get();
$latestHards = DB::table('operator_que')
->where('que_status',1)
->where('que_class','>',99)
->orderByDesc('created_at')
->limit(5)
->get();
$menuAccessService = app(MenuAccessService::class);
$isSorin = $menuAccessService->isSorin();
$visibleCities = $menuAccessService->visibleCities();
$view->with(compact(
'taskCount','taskLatest',
'hardCount','hardLatest',
'latestTasks','latestHards',
'isSorin', 'visibleCities'
));
});
} }
} }

Some files were not shown because too many files have changed in this diff Show More