| @@ -0,0 +1,90 @@ | |||
| <?php | |||
| namespace App\Console\Commands; | |||
| use App\Jobs\SeasonTicketContract\Selection\FillCandidates; | |||
| use App\Kintone\Models\DropDown\SeasonTicketContractSelection\SelectionStatus; | |||
| use App\Kintone\Models\SeasonTicketContractSelection; | |||
| use App\Util\DBUtil; | |||
| use Exception; | |||
| class SeasonTikcetContractSelectionFillCandidates extends BaseCommand | |||
| { | |||
| const COMMAND = "season-ticket-contract-selection:fill-candidates"; | |||
| /** | |||
| * The name and signature of the console command. | |||
| * | |||
| * @var string | |||
| */ | |||
| protected $signature = self::COMMAND; | |||
| /** | |||
| * The console command description. | |||
| * | |||
| * @var string | |||
| */ | |||
| protected $description = '定期選考の候補者設定のジョブを登録する'; | |||
| static public function getCommand() | |||
| { | |||
| return self::COMMAND; | |||
| } | |||
| /** | |||
| * Create a new command instance. | |||
| * | |||
| * @return void | |||
| */ | |||
| public function __construct() | |||
| { | |||
| parent::__construct(); | |||
| $this->managers = collect(); | |||
| } | |||
| /** | |||
| * Execute the console command. | |||
| * | |||
| * @return int | |||
| */ | |||
| public function service(): int | |||
| { | |||
| try { | |||
| $db = DBUtil::instance(); | |||
| $db->beginTransaction(); | |||
| $targets = $this->getTargets(); | |||
| $this->outputInfo(sprintf("取得対象 %d件", $targets->count())); | |||
| // データハンドリング | |||
| foreach ($targets as $data) { | |||
| $this->handleData($data); | |||
| } | |||
| $db->commit(); | |||
| } catch (Exception $e) { | |||
| $db->rollBack(); | |||
| throw $e; | |||
| } | |||
| return self::RESULTCODE_SUCCESS; | |||
| } | |||
| private function getTargets() | |||
| { | |||
| $access = SeasonTicketContractSelection::getAccess(); | |||
| $query = SeasonTicketContractSelection::getQuery() | |||
| ->whereIn(SeasonTicketContractSelection::FIELD_STATUS, [SelectionStatus::START]); | |||
| return $access->all($query); | |||
| } | |||
| private function handleData(SeasonTicketContractSelection $data) | |||
| { | |||
| FillCandidates::dispatch($data->getRecordId()); | |||
| } | |||
| } | |||
| @@ -2,25 +2,26 @@ | |||
| namespace App\Http\Controllers; | |||
| use Illuminate\Http\Request; | |||
| use App\Kintone\Models\Receipt; | |||
| use App\Logic\ReceiptManager; | |||
| use PDF; | |||
| class PDFController extends Controller | |||
| { | |||
| public function index() | |||
| public function entry() | |||
| { | |||
| // https://wkhtmltopdf.org/usage/wkhtmltopdf.txt | |||
| $manager = new ReceiptManager(2); | |||
| $file = $manager->makePdf(); | |||
| // $pdf = PDF::loadHTML('<h1>Hello World 岩渕</h1>'); | |||
| $pdf = PDF::loadView('pdf'); | |||
| $receipt = $manager->getReceipt(); | |||
| // キントーンへの保存 | |||
| $receipt->setFiles(Receipt::FIELD_RECEIPT_PDF_FILE, collect([$file])); | |||
| $receipt->save(); | |||
| // はがきサイズを指定 | |||
| $pdf->setOption('page-height', 148) | |||
| ->setOption('page-width', 100) | |||
| ->setOption('encoding', 'utf-8'); | |||
| return $pdf->inline(); | |||
| return response() | |||
| ->file($file->getFullPath()); | |||
| } | |||
| } | |||
| @@ -2,9 +2,9 @@ | |||
| namespace App\Http\Controllers\Web\Image; | |||
| use App\Exceptions\AppCommonException; | |||
| use App\Http\Controllers\Web\WebController; | |||
| use App\Kintone\KintoneAccess; | |||
| use App\Kintone\Models\SeasonTicketContract; | |||
| use Illuminate\Http\Request; | |||
| use Illuminate\Http\Response; | |||
| @@ -30,12 +30,21 @@ abstract class ImageController extends WebController | |||
| protected function run(Request $request): Response | |||
| { | |||
| $access = $this->getAccess(); | |||
| $file = $access->fileGet($this->param->id); | |||
| return response($file->body(), 200, [ | |||
| 'Content-Length' => $file->header('Content-Length'), | |||
| 'Content-Type' => $file->header('Content-Type'), | |||
| ]); | |||
| try { | |||
| $id = $request->route('id'); | |||
| if (!$id) { | |||
| throw new AppCommonException("パラメータ不正"); | |||
| } | |||
| $access = $this->getAccess(); | |||
| $file = $access->fileGet($id); | |||
| return response($file->body(), 200, [ | |||
| 'Content-Length' => $file->header('Content-Length'), | |||
| 'Content-Type' => $file->header('Content-Type'), | |||
| ]); | |||
| } catch (Exception $e) { | |||
| return response(); | |||
| } | |||
| } | |||
| abstract protected function getAccess(): KintoneAccess; | |||
| @@ -0,0 +1,47 @@ | |||
| <?php | |||
| namespace App\Jobs\SeasonTicketContract\Selection; | |||
| use App\Codes\QueueName; | |||
| use App\Jobs\BaseJob; | |||
| use App\Logic\SeasonTicketContractSelectionManager; | |||
| use App\Util\LoggingUtil; | |||
| use Exception; | |||
| use Illuminate\Database\Eloquent\ModelNotFoundException; | |||
| class FillCandidates extends BaseJob | |||
| { | |||
| /** | |||
| * Create a new job instance. | |||
| * | |||
| * @return void | |||
| */ | |||
| public function __construct( | |||
| private int $recordNo | |||
| ) { | |||
| $this->onQueue(QueueName::JOB->value); | |||
| } | |||
| protected function handleJob() | |||
| { | |||
| try { | |||
| $manager = new SeasonTicketContractSelectionManager($this->recordNo); | |||
| $manager->makeCandidates() | |||
| ->save(); | |||
| $selection = $manager->getSelection(); | |||
| info(sprintf( | |||
| "候補者設定 駐車場:%s 締日:%s 候補者数:%d件", | |||
| $selection->parkingName, | |||
| $selection->selectionFinalDate ? $selection->selectionFinalDate->format('Y/m/d') : "-", | |||
| $selection->candidateList->count(), | |||
| )); | |||
| } catch (ModelNotFoundException $e) { | |||
| LoggingUtil::errorException($e, sprintf("データ存在なし削除 %s", self::class)); | |||
| } catch (Exception $e) { | |||
| LoggingUtil::errorException($e, sprintf("ジョブ失敗->削除 %s", self::class)); | |||
| } | |||
| } | |||
| } | |||
| @@ -195,8 +195,16 @@ class KintoneAccess | |||
| "id" => $model->getRecordId(), | |||
| "record" => $model->getApiLayout(), | |||
| "revision" => $model->getRevision(), | |||
| ]; | |||
| logger([ | |||
| "ACTION" => "KINTONE UPDATE", | |||
| "APP" => $this->appName, | |||
| "RECORD_NO" => $model->getRecordId(), | |||
| "SEND_DATA" => $sendData, | |||
| "JSON" => json_encode($sendData, JSON_UNESCAPED_UNICODE), | |||
| ]); | |||
| $response = Http::withHeaders([ | |||
| "X-Cybozu-API-Token" => $this->apiToken, | |||
| ])->put($this->getRecordUrl(), $sendData); | |||
| @@ -32,51 +32,83 @@ class KintoneRecordQuery | |||
| return $ret; | |||
| } | |||
| /** | |||
| * @param string|\Closure(static $query): static $column | |||
| */ | |||
| public function where(string|Closure $column, string|int|null $condition = null, KintoneRecordQueryOperator $operator = KintoneRecordQueryOperator::EQ) | |||
| { | |||
| return $this->whereQuery("and", $operator->value, $column, $condition); | |||
| } | |||
| /** | |||
| * @param string|\Closure(static $query): static $column | |||
| */ | |||
| public function notWhere(string|Closure $column, string|int|null $condition = null, KintoneRecordQueryOperator $operator = KintoneRecordQueryOperator::NEQ) | |||
| { | |||
| return $this->whereQuery("and", $operator->value, $column, $condition); | |||
| } | |||
| /** | |||
| * @param string|\Closure(static $query): static $column | |||
| */ | |||
| public function orWhere(string|Closure $column, string|int|null $condition = null, KintoneRecordQueryOperator $operator = KintoneRecordQueryOperator::EQ) | |||
| { | |||
| $this->whereQuery("or", $operator->value, $column, $condition); | |||
| return $this; | |||
| } | |||
| /** | |||
| * @param string|\Closure(static $query): static $column | |||
| */ | |||
| public function notOrWhere(string|Closure $column, string|int|null $condition = null, KintoneRecordQueryOperator $operator = KintoneRecordQueryOperator::NEQ) | |||
| { | |||
| return $this->whereQuery("or", $operator->value, $column, $condition); | |||
| } | |||
| /** | |||
| * @param string|\Closure(static $query): static $column | |||
| */ | |||
| public function whereIn(string|Closure $column, array $condition) | |||
| { | |||
| return $this->whereQuery("and", "in", $column, $condition); | |||
| } | |||
| /** | |||
| * @param string|\Closure(static $query): static $column | |||
| */ | |||
| public function whereNotIn(string|Closure $column, array $condition) | |||
| { | |||
| return $this->whereQuery("and", "not in", $column, $condition); | |||
| } | |||
| /** | |||
| * @param string|\Closure(static $query): static $column | |||
| */ | |||
| public function orWhereIn(string|Closure $column, array $condition) | |||
| { | |||
| return $this->whereQuery("or", "not in", $column, $condition); | |||
| } | |||
| public function whereDate(string $column, Carbon $date, KintoneRecordQueryOperator $operator = KintoneRecordQueryOperator::EQ) | |||
| public function whereDate(string $column, Carbon $date, KintoneRecordQueryOperator $operator = KintoneRecordQueryOperator::EQ, bool $allowNull = false) | |||
| { | |||
| return $this->whereNotNull($column)->whereQuery("and", $operator->value, $column, $date->format('Y-m-d')); | |||
| if (!$allowNull) { | |||
| $this->whereNotNull($column); | |||
| } | |||
| return $this->whereQuery("and", $operator->value, $column, $date->format('Y-m-d')); | |||
| } | |||
| public function whereDateTime(string $column, Carbon $date, KintoneRecordQueryOperator $operator = KintoneRecordQueryOperator::EQ) | |||
| public function whereDateTime(string $column, Carbon $date, KintoneRecordQueryOperator $operator = KintoneRecordQueryOperator::EQ, bool $allowNull = false) | |||
| { | |||
| return $this->whereNotNull($column)->whereQuery("and", $operator->value, $column, $date->toIso8601ZuluString()); | |||
| if (!$allowNull) { | |||
| $this->whereNotNull($column); | |||
| } | |||
| return $this->whereQuery("and", $operator->value, $column, $date->toIso8601ZuluString()); | |||
| } | |||
| public function orWhereDate(string $column, Carbon $date, KintoneRecordQueryOperator $operator = KintoneRecordQueryOperator::EQ) | |||
| public function orWhereDate(string $column, Carbon $date, KintoneRecordQueryOperator $operator = KintoneRecordQueryOperator::EQ, bool $allowNull = false) | |||
| { | |||
| return $this->whereNotNull($column)->whereQuery("or", $operator->value, $column, $date->format('Y-m-d')); | |||
| if (!$allowNull) { | |||
| $this->whereNotNull($column); | |||
| } | |||
| return $this->whereQuery("or", $operator->value, $column, $date->format('Y-m-d')); | |||
| } | |||
| public function orWhereDateTime(string $column, Carbon $date, KintoneRecordQueryOperator $operator = KintoneRecordQueryOperator::EQ) | |||
| public function orWhereDateTime(string $column, Carbon $date, KintoneRecordQueryOperator $operator = KintoneRecordQueryOperator::EQ, bool $allowNull = false) | |||
| { | |||
| return $this->whereNotNull($column)->whereQuery("or", $operator->value, $column, $date->toIso8601ZuluString()); | |||
| if (!$allowNull) { | |||
| $this->whereNotNull($column); | |||
| } | |||
| return $this->whereQuery("or", $operator->value, $column, $date->toIso8601ZuluString()); | |||
| } | |||
| public function whereNull(string $column) | |||
| { | |||
| @@ -109,6 +141,9 @@ class KintoneRecordQuery | |||
| return $this; | |||
| } | |||
| /** | |||
| * @param string|\Closure(static $query): static $column | |||
| */ | |||
| private function whereQuery( | |||
| string $andOr, | |||
| string $operator, | |||
| @@ -0,0 +1,9 @@ | |||
| <?php | |||
| namespace App\Kintone\Models\DropDown\SeasonTicketContractEntry; | |||
| abstract class Status | |||
| { | |||
| const RESERVE = "予約"; | |||
| const WAIT_EMPTY = "空き待ち"; | |||
| } | |||
| @@ -0,0 +1,13 @@ | |||
| <?php | |||
| namespace App\Kintone\Models\DropDown\SeasonTicketContractSelection; | |||
| abstract class SelectionStatus | |||
| { | |||
| const START = "起票"; | |||
| const TARGET_SELECTING = "通知者選択中"; | |||
| const ENTRY_ACCEPTING = "契約希望者受付中"; | |||
| const RESULT_DECISION = "候補者仮決定"; | |||
| const FAILED = "選考不調"; | |||
| const COMPLETE = "終了"; | |||
| } | |||
| @@ -137,6 +137,12 @@ abstract class KintoneModel | |||
| public function __construct() | |||
| { | |||
| $this->data = new stdClass(); | |||
| foreach (static::FIELDS as $fileCode => $type) { | |||
| if ($type === FieldType::SUBTABLE) { | |||
| $this->set($fileCode, collect()); | |||
| } | |||
| } | |||
| } | |||
| public function __get($name) | |||
| @@ -244,12 +250,18 @@ abstract class KintoneModel | |||
| if ($type === FieldType::SUBTABLE && $this->getFieldType($fieldCode)) { | |||
| $ret = collect(); | |||
| foreach ($value as $v) { | |||
| $rowRet = | |||
| $rowArray = data_get($v, 'value'); | |||
| $rowRet = []; | |||
| $rowArray = data_get($v, 'value'); | |||
| $isEmptyRow = true; | |||
| foreach ($rowArray as $subFieldCode => $ele) { | |||
| $rowRet[$subFieldCode] = $this->readData($ele); | |||
| if (!!$rowRet[$subFieldCode]) { | |||
| $isEmptyRow = false; | |||
| } | |||
| } | |||
| if (!$isEmptyRow) { | |||
| $ret->push($this->getSubTableData($fieldCode, $rowRet)); | |||
| } | |||
| $ret->push($this->getSubTableData($fieldCode, $rowRet)); | |||
| } | |||
| return $ret; | |||
| } | |||
| @@ -332,7 +344,11 @@ abstract class KintoneModel | |||
| foreach ($data as $ele) { | |||
| if ($ele instanceof SubTableData) { | |||
| $obj = []; | |||
| $obj['value'] = $ele->toArray(); | |||
| $values = $ele->toArray(); | |||
| foreach ($values as $subFieldCode => $value) { | |||
| $obj['value'][$subFieldCode]['value'] = $value; | |||
| } | |||
| $ret[] = $obj; | |||
| } | |||
| } | |||
| @@ -12,10 +12,14 @@ class ParkingRoom extends KintoneModel | |||
| const CONFIG_KEY = "KINTONE_APP_PARKING_ROOM"; | |||
| const FIELD_PARKING_NAME = "定期駐車場"; | |||
| const FIELD_ROOM_NO = "車室番号"; | |||
| const FIELD_ROOM_TYPE = "車室タイプ"; | |||
| protected const FIELDS = [ | |||
| ...parent::FIELDS, | |||
| self::FIELD_PARKING_NAME => FieldType::SINGLE_LINE_TEXT, | |||
| self::FIELD_ROOM_NO => FieldType::SINGLE_LINE_TEXT, | |||
| self::FIELD_ROOM_TYPE => FieldType::DROP_DOWN, | |||
| ]; | |||
| protected const FIELD_NAMES = [ | |||
| @@ -7,20 +7,24 @@ use Illuminate\Support\Carbon; | |||
| /** | |||
| * アプリ名 定期申込・予約 | |||
| * @property string parkingName | |||
| * @property string status | |||
| * @property string customerName | |||
| * @property string customerNameKana | |||
| * @property string phoneNo | |||
| * @property string address | |||
| * @property string email | |||
| * @property Carbon useStartDate | |||
| * @property ?Carbon useStartDate | |||
| * @property string vehicleNo | |||
| * @property int carAmount | |||
| * @property string paymentMethod | |||
| * @property Carbon entryDatetime | |||
| */ | |||
| class SeasonTicketContractEntry extends KintoneModel | |||
| { | |||
| const CONFIG_KEY = "KINTONE_APP_SEASON_TICEKT_CONTRACT_ENTRY"; | |||
| const FIELD_PARKING_NAME = "駐車場"; | |||
| const FIELD_STATUS = "status"; | |||
| const FIELD_CUSTOMER_NAME = "氏名"; | |||
| const FIELD_CUSTOMER_NAME_KANA = "フリガナ"; | |||
| const FIELD_PHONE_NO = "電話番号"; | |||
| @@ -28,7 +32,9 @@ class SeasonTicketContractEntry extends KintoneModel | |||
| const FIELD_EMAIL = "メールアドレス"; | |||
| const FIELD_USE_START_DATE = "利用開始希望日"; | |||
| const FIELD_VEHICLE_NO = "車両番号"; | |||
| const FIELD_CAR_AMOUNT = "台数"; | |||
| const FIELD_PAYMENT_METHOD = "支払方法"; | |||
| const FIELD_ENTRY_DATETIME = "受付日時"; | |||
| protected const FIELDS = [ | |||
| ...parent::FIELDS, | |||
| @@ -41,6 +47,7 @@ class SeasonTicketContractEntry extends KintoneModel | |||
| self::FIELD_USE_START_DATE => FieldType::DATE, | |||
| self::FIELD_VEHICLE_NO => FieldType::SINGLE_LINE_TEXT, | |||
| self::FIELD_PAYMENT_METHOD => FieldType::DROP_DOWN, | |||
| self::FIELD_ENTRY_DATETIME => FieldType::DATETIME, | |||
| ]; | |||
| protected const FIELD_NAMES = [ | |||
| @@ -1,48 +0,0 @@ | |||
| <?php | |||
| namespace App\Kintone\Models; | |||
| use App\Kintone\Models\SubTable\SeasonTicketContractReserve\TargetRoom; | |||
| use Illuminate\Support\Carbon; | |||
| use Illuminate\Support\Collection; | |||
| /** | |||
| * アプリ名 定期予約選考 | |||
| * @property string parkingName | |||
| * @property Collection<int, TargetRoom> targetRoomList | |||
| */ | |||
| class SeasonTicketContractReserve extends KintoneModel | |||
| { | |||
| const CONFIG_KEY = "KINTONE_APP_SEASON_TICEKT_CONTRACT_RESERVE"; | |||
| const FIELD_PARKING_NAME = "駐車場"; | |||
| const FIELD_TARGET_ROOM_LIST = "対象車室一覧"; | |||
| const FIELD_TARGET_ROOM_LIST_ROOM_NO = "対象車室一覧_車室番号"; | |||
| const FIELD_TARGET_ROOM_LIST_ROOM_TYPE = "対象車室一覧_車室タイプ"; | |||
| const FIELD_TARGET_ROOM_LIST_RECORD_NO = "対象車室一覧_車室レコード番号"; | |||
| protected const FIELDS = [ | |||
| ...parent::FIELDS, | |||
| self::FIELD_PARKING_NAME => FieldType::SINGLE_LINE_TEXT, | |||
| self::FIELD_TARGET_ROOM_LIST => FieldType::SUBTABLE, | |||
| // self::FIELD_TARGET_ROOM_LIST => [ | |||
| // self::FIELD_TARGET_ROOM_LIST_RECORD_NO => FieldType::NUMBER, | |||
| // self::FIELD_TARGET_ROOM_LIST_ROOM_TYPE => FieldType::SINGLE_LINE_TEXT, | |||
| // self::FIELD_TARGET_ROOM_LIST_ROOM_NO => FieldType::SINGLE_LINE_TEXT, | |||
| // ], | |||
| ]; | |||
| protected const SUB_TABLES = [ | |||
| self::FIELD_TARGET_ROOM_LIST => TargetRoom::class, | |||
| ]; | |||
| protected const FIELD_NAMES = [ | |||
| ...parent::FIELD_NAMES, | |||
| ]; | |||
| protected const RELATIONS = [ | |||
| SeasonTicketContractEntry::class, | |||
| ParkingRoom::class, | |||
| ]; | |||
| } | |||
| @@ -0,0 +1,104 @@ | |||
| <?php | |||
| namespace App\Kintone\Models; | |||
| use App\Kintone\Models\SubTable\SeasonTicketContractSelection\Candidate; | |||
| use App\Kintone\Models\SubTable\SeasonTicketContractSelection\Entry; | |||
| use App\Kintone\Models\SubTable\SeasonTicketContractSelection\Result; | |||
| use App\Kintone\Models\SubTable\SeasonTicketContractSelection\TargetRoom; | |||
| use Illuminate\Support\Carbon; | |||
| use Illuminate\Support\Collection; | |||
| /** | |||
| * アプリ名 定期予約選考 | |||
| * @property string parkingName | |||
| * @property string status | |||
| * @property ?Carbon useStartDate | |||
| * @property ?Carbon selectionFinalDate | |||
| * @property Collection<int, TargetRoom> targetRoomList | |||
| * @property Collection<int, Candidate> candidateList | |||
| * @property Collection<int, Entry> entryList | |||
| * @property Collection<int, Result> resultList | |||
| * @property string selectionMessage | |||
| */ | |||
| class SeasonTicketContractSelection extends KintoneModel | |||
| { | |||
| const CONFIG_KEY = "KINTONE_APP_SEASON_TICEKT_CONTRACT_SELECTION"; | |||
| const FIELD_PARKING_NAME = "駐車場名"; | |||
| const FIELD_STATUS = "選考ステータス"; | |||
| const FIELD_USE_START_DATE = "利用開始日"; | |||
| const FIELD_SELECTION_FINAL_DATE = "選考締切日"; | |||
| const FIELD_TARGET_ROOM_LIST = "対象車室一覧"; | |||
| const FIELD_TARGET_ROOM_LIST_ROOM_NO = "対象車室一覧_車室番号"; | |||
| const FIELD_TARGET_ROOM_LIST_ROOM_TYPE = "対象車室一覧_車室タイプ"; | |||
| const FIELD_TARGET_ROOM_LIST_RECORD_NO = "対象車室一覧_車室レコード番号"; | |||
| const FIELD_CANDIDATE_LIST = "申込者一覧"; | |||
| const FIELD_CANDIDATE_EMAIL_SEND_TARGET = "申込者一覧_通知対象"; | |||
| const FIELD_CANDIDATE_LIST_RECORD_NO = "申込者一覧_申込レコード番号"; | |||
| const FIELD_CANDIDATE_LIST_STATUS = "申込者一覧_状態"; | |||
| const FIELD_CANDIDATE_LIST_PLAN = "申込者一覧_プラン"; | |||
| const FIELD_CANDIDATE_LIST_ENTRY_NO = "申込者一覧_申込番号"; | |||
| const FIELD_CANDIDATE_LIST_NAME = "申込者一覧_氏名"; | |||
| const FIELD_CANDIDATE_LIST_ENTRY_DATETIME = "申込者一覧_受付日時"; | |||
| const FIELD_CANDIDATE_LIST_WANTS_TO_USE_START_DATE = "申込者一覧_利用開始希望日"; | |||
| const FIELD_CANDIDATE_LIST_CAR_AMOUNT = "申込者一覧_台数"; | |||
| const FIELD_CANDIDATE_LIST_EMAIL = "申込者一覧_メールアドレス"; | |||
| const FIELD_ENTRY_LIST = "契約希望者一覧"; | |||
| const FIELD_ENTRY_LIST_STATUS = "契約希望者一覧_状態"; | |||
| const FIELD_ENTRY_LIST_PLAN = "契約希望者一覧_プラン"; | |||
| const FIELD_ENTRY_LIST_RECORD_NO = "契約希望者一覧_申込レコード番号"; | |||
| const FIELD_ENTRY_LIST_ENTRY_NO = "契約希望者一覧_申込番号"; | |||
| const FIELD_ENTRY_LIST_NAME = "契約希望者一覧_氏名"; | |||
| const FIELD_ENTRY_LIST_ENTRY_DATETIME = "契約希望者一覧_受付日時"; | |||
| const FIELD_ENTRY_LIST_WANTS_TO_USE_START_DATE = "契約希望者一覧_利用開始希望日"; | |||
| const FIELD_ENTRY_LIST_CAR_AMOUNT = "契約希望者一覧_台数"; | |||
| const FIELD_RESULT_LIST = "選考結果一覧"; | |||
| const FIELD_RESULT_LIST_ROOM_RECORD_NO = "選考結果一覧_車室レコード番号"; | |||
| const FIELD_RESULT_LIST_ROOM_NO = "選考結果一覧_車室番号"; | |||
| const FIELD_RESULT_LIST_ROOM_TYPE = "選考結果一覧_車室タイプ"; | |||
| const FIELD_RESULT_LIST_ENTRY_RECORD_NO = "選考結果一覧_申込レコード番号"; | |||
| const FIELD_RESULT_LIST_STATUS = "選考結果一覧_ステータス"; | |||
| const FIELD_RESULT_LIST_PLAN = "選考結果一覧_プラン"; | |||
| const FIELD_RESULT_LIST_ENTRY_NO = "選考結果一覧_申込番号"; | |||
| const FIELD_RESULT_LIST_NAME = "選考結果一覧_氏名"; | |||
| const FIELD_SELECTION_MESSAGE = "自動選考メッセージ"; | |||
| const FIELD_SEARCH_CANDIDATE_LIST_RECORD_NO = "検索用_申込一覧_レコード番号"; | |||
| const FIELD_SEARCH_ENTRY_LIST_RECORD_NO = "検索用_契約希望者_レコード番号"; | |||
| const FIELD_SEARCH_RESULT_LIST_ENTRY_RECORD_NO = "検索用_選考結果_レコード番号"; | |||
| protected const FIELDS = [ | |||
| ...parent::FIELDS, | |||
| self::FIELD_PARKING_NAME => FieldType::SINGLE_LINE_TEXT, | |||
| self::FIELD_STATUS => FieldType::DROP_DOWN, | |||
| self::FIELD_TARGET_ROOM_LIST => FieldType::SUBTABLE, | |||
| self::FIELD_CANDIDATE_LIST => FieldType::SUBTABLE, | |||
| self::FIELD_ENTRY_LIST => FieldType::SUBTABLE, | |||
| self::FIELD_RESULT_LIST => FieldType::SUBTABLE, | |||
| self::FIELD_SELECTION_MESSAGE => FieldType::MULTI_LINE_TEXT, | |||
| ]; | |||
| protected const SUB_TABLES = [ | |||
| self::FIELD_TARGET_ROOM_LIST => TargetRoom::class, | |||
| self::FIELD_CANDIDATE_LIST => Candidate::class, | |||
| self::FIELD_ENTRY_LIST => Entry::class, | |||
| self::FIELD_RESULT_LIST => Result::class, | |||
| ]; | |||
| protected const FIELD_NAMES = [ | |||
| ...parent::FIELD_NAMES, | |||
| ]; | |||
| protected const RELATIONS = [ | |||
| SeasonTicketContractEntry::class, | |||
| ParkingRoom::class, | |||
| ]; | |||
| } | |||
| @@ -1,31 +0,0 @@ | |||
| <?php | |||
| namespace App\Kintone\Models\SubTable\SeasonTicketContractReserve; | |||
| use App\Kintone\Models\SeasonTicketContractReserve; | |||
| use App\Kintone\Models\SubTable\SubTableData; | |||
| class TargetRoom extends SubTableData | |||
| { | |||
| public string $roomNo; | |||
| public string $roomType; | |||
| public int $roomRecordNo; | |||
| public function __construct(array $data) | |||
| { | |||
| $this->roomNo = data_get($data, SeasonTicketContractReserve::FIELD_TARGET_ROOM_LIST_ROOM_NO, ""); | |||
| $this->roomType = data_get($data, SeasonTicketContractReserve::FIELD_TARGET_ROOM_LIST_ROOM_TYPE, ""); | |||
| $this->roomRecordNo = intval(data_get($data, SeasonTicketContractReserve::FIELD_TARGET_ROOM_LIST_RECORD_NO, 0)); | |||
| parent::__construct($data); | |||
| } | |||
| public function toArray(): array | |||
| { | |||
| return [ | |||
| SeasonTicketContractReserve::FIELD_TARGET_ROOM_LIST_ROOM_NO => $this->roomNo, | |||
| SeasonTicketContractReserve::FIELD_TARGET_ROOM_LIST_ROOM_TYPE => $this->roomType, | |||
| SeasonTicketContractReserve::FIELD_TARGET_ROOM_LIST_RECORD_NO => $this->roomRecordNo, | |||
| ]; | |||
| } | |||
| } | |||
| @@ -0,0 +1,47 @@ | |||
| <?php | |||
| namespace App\Kintone\Models\SubTable\SeasonTicketContractSelection; | |||
| use App\Kintone\Models\SeasonTicketContractSelection; | |||
| use App\Kintone\Models\SubTable\SubTableData; | |||
| use App\Util\DateUtil; | |||
| use Illuminate\Support\Carbon; | |||
| class Candidate extends SubTableData | |||
| { | |||
| public bool $emailSendTarget; | |||
| public int $entryRecordNo; | |||
| public string $status; | |||
| public string $planName; | |||
| public string $entryNo; | |||
| public string $name; | |||
| public ?Carbon $entryDatetime; | |||
| public ?Carbon $wantsToUseStartDate; | |||
| public int $carAmount; | |||
| public string $email; | |||
| public function __construct(array $data = []) | |||
| { | |||
| $this->emailSendTarget = count(data_get($data, SeasonTicketContractSelection::FIELD_CANDIDATE_EMAIL_SEND_TARGET, [])) !== 0; | |||
| $this->entryRecordNo = intval(data_get($data, SeasonTicketContractSelection::FIELD_CANDIDATE_LIST_RECORD_NO, 0)); | |||
| $this->planName = data_get($data, SeasonTicketContractSelection::FIELD_CANDIDATE_LIST_STATUS, ""); | |||
| $this->planName = data_get($data, SeasonTicketContractSelection::FIELD_CANDIDATE_LIST_PLAN, ""); | |||
| $this->entryNo = data_get($data, SeasonTicketContractSelection::FIELD_CANDIDATE_LIST_PLAN, ""); | |||
| $this->name = data_get($data, SeasonTicketContractSelection::FIELD_CANDIDATE_LIST_NAME, ""); | |||
| $this->entryDatetime = DateUtil::parse(data_get($data, SeasonTicketContractSelection::FIELD_TARGET_ROOM_LIST_ROOM_NO, "")); | |||
| $this->wantsToUseStartDate = DateUtil::parse(data_get($data, SeasonTicketContractSelection::FIELD_CANDIDATE_LIST_WANTS_TO_USE_START_DATE, "")); | |||
| $this->carAmount = intval(data_get($data, SeasonTicketContractSelection::FIELD_CANDIDATE_LIST_CAR_AMOUNT, 0)); | |||
| $this->email = data_get($data, SeasonTicketContractSelection::FIELD_TARGET_ROOM_LIST_ROOM_NO, ""); | |||
| parent::__construct($data); | |||
| } | |||
| public function toArray(): array | |||
| { | |||
| return [ | |||
| SeasonTicketContractSelection::FIELD_CANDIDATE_EMAIL_SEND_TARGET => $this->emailSendTarget ? ["〇"] : [], | |||
| SeasonTicketContractSelection::FIELD_CANDIDATE_LIST_RECORD_NO => $this->entryRecordNo, | |||
| ]; | |||
| } | |||
| } | |||
| @@ -0,0 +1,42 @@ | |||
| <?php | |||
| namespace App\Kintone\Models\SubTable\SeasonTicketContractSelection; | |||
| use App\Kintone\Models\SeasonTicketContractSelection; | |||
| use App\Kintone\Models\SubTable\SubTableData; | |||
| use App\Util\DateUtil; | |||
| use Illuminate\Support\Carbon; | |||
| class Entry extends SubTableData | |||
| { | |||
| public int $entryRecordNo; | |||
| public string $status; | |||
| public string $planName; | |||
| public string $entryNo; | |||
| public string $name; | |||
| public ?Carbon $entryDatetime; | |||
| public ?Carbon $wantsToUseStartDate; | |||
| public int $carAmount; | |||
| public function __construct(array $data = []) | |||
| { | |||
| $this->entryRecordNo = intval(data_get($data, SeasonTicketContractSelection::FIELD_ENTRY_LIST_RECORD_NO, 0)); | |||
| $this->status = data_get($data, SeasonTicketContractSelection::FIELD_ENTRY_LIST_STATUS, ""); | |||
| $this->planName = data_get($data, SeasonTicketContractSelection::FIELD_ENTRY_LIST_PLAN, ""); | |||
| $this->entryNo = data_get($data, SeasonTicketContractSelection::FIELD_ENTRY_LIST_ENTRY_NO, ""); | |||
| $this->name = data_get($data, SeasonTicketContractSelection::FIELD_ENTRY_LIST_NAME, ""); | |||
| $this->entryDatetime = DateUtil::parse(data_get($data, SeasonTicketContractSelection::FIELD_ENTRY_LIST_ENTRY_DATETIME, "")); | |||
| $this->wantsToUseStartDate = DateUtil::parse(data_get($data, SeasonTicketContractSelection::FIELD_ENTRY_LIST_WANTS_TO_USE_START_DATE, "")); | |||
| $this->carAmount = intval(data_get($data, SeasonTicketContractSelection::FIELD_ENTRY_LIST_CAR_AMOUNT, 0)); | |||
| parent::__construct($data); | |||
| } | |||
| public function toArray(): array | |||
| { | |||
| return [ | |||
| SeasonTicketContractSelection::FIELD_ENTRY_LIST_RECORD_NO => $this->entryRecordNo, | |||
| ]; | |||
| } | |||
| } | |||
| @@ -0,0 +1,42 @@ | |||
| <?php | |||
| namespace App\Kintone\Models\SubTable\SeasonTicketContractSelection; | |||
| use App\Kintone\Models\SeasonTicketContractSelection; | |||
| use App\Kintone\Models\SubTable\SubTableData; | |||
| class Result extends SubTableData | |||
| { | |||
| public int $roomRecordNo; | |||
| public string $roomNo; | |||
| public string $roomType; | |||
| public int $entryRecordNo; | |||
| public string $status; | |||
| public string $planName; | |||
| public string $entryNo; | |||
| public string $name; | |||
| public function __construct(array $data = []) | |||
| { | |||
| $this->roomRecordNo = intval(data_get($data, SeasonTicketContractSelection::FIELD_RESULT_LIST_ROOM_RECORD_NO, 0)); | |||
| $this->roomNo = data_get($data, SeasonTicketContractSelection::FIELD_RESULT_LIST_ROOM_NO, ""); | |||
| $this->roomType = data_get($data, SeasonTicketContractSelection::FIELD_RESULT_LIST_ROOM_TYPE, ""); | |||
| $this->entryRecordNo = intval(data_get($data, SeasonTicketContractSelection::FIELD_RESULT_LIST_ENTRY_RECORD_NO, 0)); | |||
| $this->status = data_get($data, SeasonTicketContractSelection::FIELD_RESULT_LIST_STATUS, ""); | |||
| $this->planName = data_get($data, SeasonTicketContractSelection::FIELD_RESULT_LIST_PLAN, ""); | |||
| $this->entryNo = data_get($data, SeasonTicketContractSelection::FIELD_RESULT_LIST_ENTRY_NO, ""); | |||
| $this->name = data_get($data, SeasonTicketContractSelection::FIELD_RESULT_LIST_NAME, ""); | |||
| parent::__construct($data); | |||
| } | |||
| public function toArray(): array | |||
| { | |||
| return [ | |||
| SeasonTicketContractSelection::FIELD_RESULT_LIST_ROOM_RECORD_NO => $this->roomRecordNo, | |||
| SeasonTicketContractSelection::FIELD_RESULT_LIST_ENTRY_RECORD_NO => $this->entryRecordNo, | |||
| ]; | |||
| } | |||
| } | |||
| @@ -0,0 +1,31 @@ | |||
| <?php | |||
| namespace App\Kintone\Models\SubTable\SeasonTicketContractSelection; | |||
| use App\Kintone\Models\SeasonTicketContractSelection; | |||
| use App\Kintone\Models\SubTable\SubTableData; | |||
| class TargetRoom extends SubTableData | |||
| { | |||
| public string $roomNo; | |||
| public string $roomType; | |||
| public int $roomRecordNo; | |||
| public function __construct(array $data = []) | |||
| { | |||
| $this->roomNo = data_get($data, SeasonTicketContractSelection::FIELD_TARGET_ROOM_LIST_ROOM_NO, ""); | |||
| $this->roomType = data_get($data, SeasonTicketContractSelection::FIELD_TARGET_ROOM_LIST_ROOM_TYPE, ""); | |||
| $this->roomRecordNo = intval(data_get($data, SeasonTicketContractSelection::FIELD_TARGET_ROOM_LIST_RECORD_NO, 0)); | |||
| parent::__construct($data); | |||
| } | |||
| public function toArray(): array | |||
| { | |||
| return [ | |||
| SeasonTicketContractSelection::FIELD_TARGET_ROOM_LIST_ROOM_NO => $this->roomNo, | |||
| SeasonTicketContractSelection::FIELD_TARGET_ROOM_LIST_ROOM_TYPE => $this->roomType, | |||
| SeasonTicketContractSelection::FIELD_TARGET_ROOM_LIST_RECORD_NO => $this->roomRecordNo, | |||
| ]; | |||
| } | |||
| } | |||
| @@ -0,0 +1,242 @@ | |||
| <?php | |||
| namespace App\Logic; | |||
| use App\Exceptions\AppCommonException; | |||
| use App\Kintone\KintoneRecordQuery; | |||
| use App\Kintone\KintoneRecordQueryOperator; | |||
| use App\Kintone\Models\DropDown\SeasonTicketContractEntry\Status; | |||
| use App\Kintone\Models\SeasonTicketContractEntry as EntryModel; | |||
| use App\Kintone\Models\SeasonTicketContractSelection as SelectionModel; | |||
| use App\Kintone\Models\SubTable\SeasonTicketContractSelection\Entry; | |||
| use App\Kintone\Models\DropDown\SeasonTicketContractSelection\SelectionStatus; | |||
| use App\Kintone\Models\SubTable\SeasonTicketContractSelection\Candidate; | |||
| use App\Kintone\Models\SubTable\SeasonTicketContractSelection\Result; | |||
| use App\Kintone\Models\SubTable\SeasonTicketContractSelection\TargetRoom; | |||
| use App\Util\DateUtil; | |||
| use Illuminate\Support\Collection; | |||
| class SeasonTicketContractSelectionManager | |||
| { | |||
| private SelectionModel $selection; | |||
| public function __construct(?int $recordNo = null) | |||
| { | |||
| if ($recordNo) { | |||
| $this->selection = SelectionModel::find($recordNo); | |||
| } else { | |||
| $this->selection = new SelectionModel(); | |||
| $this->selection->status = SelectionStatus::START; | |||
| } | |||
| } | |||
| public function makeCandidates() | |||
| { | |||
| if ($this->selection->status !== SelectionStatus::START) { | |||
| new AppCommonException( | |||
| sprintf( | |||
| "ステータス不正 候補者一覧を設定するにはステータスが%[s]である必要がある。現ステータス[%s]", | |||
| SelectionStatus::START, | |||
| $this->selection->status, | |||
| ) | |||
| ); | |||
| } | |||
| $candidateList = $this->selection->candidateList->empty(); | |||
| // 対象の申込を取得する | |||
| // TODO 空き車室に対応したプランの申込に絞る必要あり | |||
| $access = EntryModel::getAccess(); | |||
| $getBaseQuery = function () { | |||
| $baseQuery = EntryModel::getQuery(); | |||
| $baseQuery->where(EntryModel::FIELD_PARKING_NAME, $this->selection->parkingName) | |||
| ->orderByAsc(EntryModel::FIELD_ENTRY_DATETIME); | |||
| if ($this->selection->useStartDate) { | |||
| $useStartDateQuery = function (KintoneRecordQuery $query) { | |||
| return $query->whereNull(EntryModel::FIELD_USE_START_DATE) | |||
| ->orWhereDate(EntryModel::FIELD_USE_START_DATE, $this->selection->useStartDate, KintoneRecordQueryOperator::GE, true); | |||
| }; | |||
| $baseQuery->where($useStartDateQuery); | |||
| } | |||
| return $baseQuery; | |||
| }; | |||
| $reserveQuery = $getBaseQuery()->whereIn(EntryModel::FIELD_STATUS, [Status::RESERVE]); | |||
| $waitQuery = $getBaseQuery()->whereIn(EntryModel::FIELD_STATUS, [Status::WAIT_EMPTY]); | |||
| $reserveList = $access->all($reserveQuery); | |||
| $waitList = $access->all($waitQuery); | |||
| $list = $reserveList->merge($waitList); | |||
| foreach ($list as $ele) { | |||
| $candidate = new Candidate(); | |||
| $candidate->entryRecordNo = $ele->getRecordId(); | |||
| $candidateList->push($candidate); | |||
| } | |||
| $this->selection->candidateList = $candidateList; | |||
| $this->selection->status = SelectionStatus::TARGET_SELECTING; | |||
| return $this; | |||
| } | |||
| public function sendNotine() | |||
| { | |||
| // メール送信 | |||
| return $this; | |||
| } | |||
| public function makeResult() | |||
| { | |||
| $messages = $this->doSelection(); | |||
| $this->selection->selectionMessage = $messages->implode("\r\n"); | |||
| return $this; | |||
| } | |||
| public function save() | |||
| { | |||
| // レコード保存 | |||
| $this->selection->save(); | |||
| return $this; | |||
| } | |||
| public function entry(int $recordNo, string $hashPassword) | |||
| { | |||
| if (!$this->checkHash($recordNo, $hashPassword)) { | |||
| throw new AppCommonException("認証エラー"); | |||
| } | |||
| $list = $this->selection->entryList; | |||
| if ($this->hasAlreadyEnterd($recordNo)) { | |||
| // すでにエントリー済みのためスキップ | |||
| return $this; | |||
| } | |||
| $entry = new Entry(); | |||
| $entry->entryRecordNo = $recordNo; | |||
| $list->push($entry); | |||
| $this->selection->entryList = $list; | |||
| return $this; | |||
| } | |||
| public function hasAlreadyEnterd(int $recordNo): bool | |||
| { | |||
| $list = $this->selection->entryList; | |||
| return $list->search(function (Entry $item) use ($recordNo) { | |||
| return $recordNo === $item->entryRecordNo; | |||
| }) !== false; | |||
| } | |||
| public function getSelection() | |||
| { | |||
| return $this->selection; | |||
| } | |||
| public function getHash(int $recordNo): string | |||
| { | |||
| $source = sprintf("%010d-%010d", $recordNo, intval($this->selection->getRecordId())); | |||
| return hash('sha256', $source); | |||
| } | |||
| private function checkHash(int $recordNo, string $hash): bool | |||
| { | |||
| $expect = $this->getHash($recordNo); | |||
| return $expect === $hash; | |||
| } | |||
| // 抽選用------------------------ | |||
| private function doSelection() | |||
| { | |||
| /** | |||
| * @var Collection<int, string> | |||
| */ | |||
| $messages = collect(); | |||
| $resultList = $this->selection->resultList->empty(); | |||
| // 候補者 | |||
| if ($this->selection->entryList->isEmpty()) { | |||
| $this->selection->status = SelectionStatus::FAILED; | |||
| $messages->push("契約希望者不在のため、選考不調"); | |||
| return $messages; | |||
| } | |||
| // 希望者一覧のコピー | |||
| $entryList = $this->selection->entryList->empty(); | |||
| foreach ($this->selection->entryList as $entry) { | |||
| $entryList->push($entry); | |||
| } | |||
| // 選考結果の設定 | |||
| $totalRoomAmount = 0; | |||
| foreach ($this->selection->targetRoomList as $room) { | |||
| $entry = $this->selectionByRoom($room, $entryList); | |||
| if ($entry) { | |||
| $result = new Result(); | |||
| $result->entryRecordNo = $entry->entryRecordNo; | |||
| $result->roomRecordNo = $room->roomRecordNo; | |||
| $totalRoomAmount += $entry->carAmount; | |||
| $resultList->push($result); | |||
| } | |||
| } | |||
| if ($this->selection->targetRoomList->count() < $totalRoomAmount) { | |||
| $messages->push("!警告:空き枠以上の台数を抽選したため調整してください"); | |||
| } | |||
| $this->selection->status = $resultList->isNotEmpty() ? | |||
| SelectionStatus::COMPLETE : SelectionStatus::FAILED; | |||
| $this->selection->resultList = $resultList; | |||
| return $messages; | |||
| } | |||
| /** | |||
| * @param TargetRoom $room | |||
| * @param Collection<int, Entry> $entries | |||
| */ | |||
| private function selectionByRoom(TargetRoom $room, Collection $entries): ?Entry | |||
| { | |||
| // 予約者確認 | |||
| $entry = $entries->filter(function (Entry $entry) { | |||
| return $entry->status === Status::RESERVE; | |||
| })->sort(function (Entry $a, Entry $b) { | |||
| return $a->entryDatetime->lt($b->entryDatetime) ? -1 : 1; | |||
| })->first(); | |||
| if ($entry) { | |||
| return $entry; | |||
| } | |||
| // 空き待ちで抽選 | |||
| $entry = $entries->filter(function (Entry $entry) { | |||
| return $entry->status === Status::WAIT_EMPTY; | |||
| })->shuffle(intval(DateUtil::now()->timestamp))->first(); | |||
| if ($entry) { | |||
| return $entry; | |||
| } | |||
| return null; | |||
| } | |||
| } | |||
| @@ -24,8 +24,11 @@ class DateUtil | |||
| return new Carbon(); | |||
| } | |||
| public static function parse(string $source): Carbon|null | |||
| public static function parse(?string $source): Carbon|null | |||
| { | |||
| if ($source === null) { | |||
| return null; | |||
| } | |||
| $date = Carbon::parse($source); | |||
| if ($date->isValid()) { | |||
| return $date->timezone(config('app.timezone')); | |||
| @@ -0,0 +1,41 @@ | |||
| <?php | |||
| namespace App\Util; | |||
| use Exception; | |||
| use Illuminate\Support\Facades\Log; | |||
| class LoggingUtil | |||
| { | |||
| public static function infoException(Exception $e, string|array $messages = []) | |||
| { | |||
| Log::error(self::getExceptionContents($e, $messages)); | |||
| } | |||
| public static function warnException(Exception $e, string|array $messages = []) | |||
| { | |||
| Log::error(self::getExceptionContents($e, $messages)); | |||
| } | |||
| public static function errorException(Exception $e, string|array $messages = []) | |||
| { | |||
| Log::error(self::getExceptionContents($e, $messages)); | |||
| } | |||
| private static function getExceptionContents(Exception $e, string|array $messages) | |||
| { | |||
| if (is_string($messages)) { | |||
| $message = $messages; | |||
| $messages = []; | |||
| $messages[] = $message; | |||
| } | |||
| return [ | |||
| ...$messages, | |||
| '_message' => $e->getMessage(), | |||
| '_file' => $e->getFile(), | |||
| '_line' => $e->getLine(), | |||
| '_exceptionType' => $e::class, | |||
| ]; | |||
| } | |||
| } | |||
| @@ -33,7 +33,7 @@ return [ | |||
| ...App\Kintone\Models\SeasonTicketContract::setConfig(), | |||
| ...App\Kintone\Models\SeasonTicketContractPlan::setConfig(), | |||
| ...App\Kintone\Models\SeasonTicketContractEntry::setConfig(), | |||
| ...App\Kintone\Models\SeasonTicketContractReserve::setConfig(), | |||
| ...App\Kintone\Models\SeasonTicketContractSelection::setConfig(), | |||
| ...App\Kintone\Models\PaymentPlan::setConfig(), | |||
| ...App\Kintone\Models\GeneralApplication::setConfig(), | |||
| ...App\Kintone\Models\FAQ::setConfig(), | |||