From 56384c1fa9f4d62e2e1d43dce5a268b442d85550 Mon Sep 17 00:00:00 2001 From: ricky rx Date: Fri, 21 Jun 2024 16:43:56 +0700 Subject: [PATCH] feat: tv with import export excel --- app/Helper/DatabaseHelper.php | 8 + app/Helper/Frontend/ApiUtilities.php | 48 ++++ .../api/superadmin/tv/TvController.php | 19 +- app/Models/ApkUpdate.php | 2 +- app/Models/Tv.php | 231 +++++++++++++++++- app/Models/TvLog.php | 33 +++ database/migrations/2024_05_18_033105_tv.php | 10 + routes/api/superadmin.php | 1 + 8 files changed, 336 insertions(+), 16 deletions(-) create mode 100644 app/Helper/Frontend/ApiUtilities.php create mode 100644 app/Models/TvLog.php diff --git a/app/Helper/DatabaseHelper.php b/app/Helper/DatabaseHelper.php index 9c2d82e..7145de8 100644 --- a/app/Helper/DatabaseHelper.php +++ b/app/Helper/DatabaseHelper.php @@ -25,5 +25,13 @@ ]; } public static function getSearchValidation() { return 'nullable|string'; } + + public static function compileDirtyEloquentToArrMessage($model) { + $dirties = $model->getDirty(); + foreach($dirties as $key => $dirty) { + $dirties[$key] = $model->getOriginal($key) . ' => ' . $dirty; + } + return $dirties; + } } ?> \ No newline at end of file diff --git a/app/Helper/Frontend/ApiUtilities.php b/app/Helper/Frontend/ApiUtilities.php new file mode 100644 index 0000000..6f70f42 --- /dev/null +++ b/app/Helper/Frontend/ApiUtilities.php @@ -0,0 +1,48 @@ +validate(['a' => 'nullable|string']); + + switch($request->a) { + case 'excelTemplate': + return Tv::getExcelTemplate(); + break; + case 'validateData': + $tvCodes = $request->tvCodes ?? []; + $tvs = $request->tvs ?? []; + $oValidation = TV::validateExcel($tvs, $tvCodes); + return JSONResponse::Success(['oValidation' => $oValidation]); + break; + case 'uploadExcel': + $tvCodes = $request->tvCodes ?? []; + $tvs = $request->tvs ?? []; + $oValidation = TV::validateExcel($tvs, $tvCodes); + $result = TV::uploadExcel($tvs, $oValidation); + return JSONResponse::Success($result); + break; + case 'exportData': + return Tv::getExportData($request); + break; + case 'excelDetail': + return Tv::getExcelDetail($request); + break; + + } + throw new \Exception('Invalid Request Command'); + } +} +?> \ No newline at end of file diff --git a/app/Http/Controllers/api/superadmin/tv/TvController.php b/app/Http/Controllers/api/superadmin/tv/TvController.php index 41c3544..5ddb537 100644 --- a/app/Http/Controllers/api/superadmin/tv/TvController.php +++ b/app/Http/Controllers/api/superadmin/tv/TvController.php @@ -3,8 +3,10 @@ namespace App\Http\Controllers\api\superadmin\tv; use App\Helper\DatabaseHelper; +use App\Helper\Frontend\ApiUtilities; use App\Helper\JSONResponse; use App\Http\Controllers\Controller; +use App\Models\ApkUpdate; use App\Models\Tv; use Illuminate\Http\Request; @@ -12,17 +14,20 @@ class TvController extends Controller { public function init(Request $request) { $request->validate([ 'perPage' => 'nullable|integer|min:1', - ...DatabaseHelper::getOrderBysValidations(), - 'search' => DatabaseHelper::getSearchValidation() + 'isFirstTime' => 'nullable|boolean', ]); - $newTvRequests = Tv::multiSearch($request->search, ['code']) - ->multiOrderBy($request->orderBys, 'created_at desc') - ->paginate($request->perPage ?? 10); - - return JSONResponse::Success(['data' => $newTvRequests ]); + $additionalData = []; + if($request->isFirstTime) { + $additionalData['apkUpdates'] = ApkUpdate::select('version_code', 'version_name') + ->orderBy('version_code', 'desc') + ->get(); + } + $newTvRequests = TV::validateAndGetEloquentFromRequest($request)->paginate($request->perPage ?? 10); + return JSONResponse::Success(['data' => $newTvRequests, ...$additionalData ]); } + public function excel(Request $request) { return ApiUtilities::tvExcel($request); } public function update(Request $request) { return Tv::updateFromRequest($request); } public function changeStatus(Request $request) { return Tv::changeStatusFromRequest($request); } } diff --git a/app/Models/ApkUpdate.php b/app/Models/ApkUpdate.php index ee8d556..05cdf84 100644 --- a/app/Models/ApkUpdate.php +++ b/app/Models/ApkUpdate.php @@ -47,7 +47,7 @@ class ApkUpdate extends Model { 'id' => 'nullable|integer|exists:App\Models\ApkUpdate,id', 'name' => 'required|string', 'file' => 'required_without:id|file|' . FileHelper::convertToStrLaraValidation(FileHelper::$allowedApkExtensions), - 'version_code' => 'required|integer|min:1', + 'version_code' => 'required|integer|min:1|unique:apk_updates,version_code', 'version_name' => 'required|string', 'change_note' => 'nullable|string' ], [ diff --git a/app/Models/Tv.php b/app/Models/Tv.php index a71c664..61d8bbf 100644 --- a/app/Models/Tv.php +++ b/app/Models/Tv.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Helper\DatabaseHelper; use App\Helper\JSONResponse; use App\Helper\STS\Indokargo; use App\Helper\Traits\Models\CanMultiOrderBy; @@ -14,8 +15,10 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; use Illuminate\Support\Str; +use Illuminate\Validation\ValidationException; class Tv extends Model { use HasFactory; @@ -43,14 +46,14 @@ class Tv extends Model { // -- RELATED TO DATA FUNCTION public static function generateUniqueCode() { // init - $totalSuffixDigit = 5; + $countSuffixDigit = 5; date_default_timezone_set('Asia/Jakarta'); $prefixCode = 'TV' . date('Ym'); // try to get unique suffix $uniqueCode = ''; while(true) { - $suffixCode = strtoupper(Str::random($totalSuffixDigit)); + $suffixCode = strtoupper(Str::random($countSuffixDigit)); $uniqueCode = $prefixCode . $suffixCode; $isUnique = TV::where('code', $uniqueCode)->first(); if(!$isUnique) break; @@ -62,14 +65,39 @@ class Tv extends Model { //------------------------------------------------------------ // -- RELATED TO REQUEST + public static function validateAndGetEloquentFromRequest(Request $request) { + $request->validate([ + ...DatabaseHelper::getOrderBysValidations(), + 'search' => DatabaseHelper::getSearchValidation(), + 'apkVersionCode' => 'nullable|integer', + 'isActive' => 'nullable|boolean', + 'lastConnectedAt' => 'nullable|array', + 'lastConnectedAt.from' => 'nullable|date', + 'lastConnectedAt.to' => 'nullable|date' + ]); + + return Tv::when($request->isActive != null, function($q) use($request) { + $q->where('is_active', $request->isActive); + }) + ->when($request->apkVersionCode, function($q, $apkVersionCode) { + $q->where('apk_version_code', $apkVersionCode); + }) + ->when($request->lastConnectedAt['from'] ?? '', function($q, $from) { + $q->where(DB::raw('DATE(last_connected_at)'), '>=', $from); + }) + ->when($request->lastConnectedAt['to'] ?? '', function($q, $to) { + $q->where(DB::raw('DATE(last_connected_at)'), '<=', $to); + }) + ->multiSearch($request->search, ['code', 'company_name']) + ->multiOrderBy($request->orderBys, 'created_at desc'); + } + public static function updateFromRequest(Request $request) { if($request->code) $request->merge(['code' => strtoupper($request->code)]); $request->validate([ 'id' => 'required|integer|exists:App\Models\Tv', 'code' => ['required','string',Rule::unique('tvs', 'code') - ->when($request->id, function($q, $id) { - $q->whereNot('id', $id); - })], + ->when($request->id, function($q, $id) { $q->whereNot('id', $id);})], 'company_name' => 'nullable|string', 'address' => 'nullable|string', 'street_address' => 'nullable|string', @@ -108,7 +136,7 @@ class Tv extends Model { DB::commit(); return JSONResponse::Success(['message'=>'Success to update tv data']); - // TODO: waiting from ops workflow + // TODO: waiting from ops workflow (Dont forget to resync co_name, address, street address in ik) // // try to sys_to_sys with indokargo // $jsonResponse = Indokargo::updateTVAddress($request, $tv->id, $tv->ik_address_id); // DB::commit(); @@ -129,7 +157,7 @@ class Tv extends Model { DB::commit(); return JSONResponse::Success(['message'=>'Success to change tv status']); - // TODO: waiting from ops workflow + // TODO: waiting from ops workflow (Dont forget to resync co_name, address, street address in ik) // // try to sys_to_sys with indokargo // $jsonResponse = Indokargo::changeStatusAddress(new Request(['is_active' => $tv->is_active]), // $tv->id, $tv->ik_address_id); @@ -140,6 +168,193 @@ class Tv extends Model { throw $th; } } - // -- END RELATED TO REQUES + // -- END RELATED TO REQUEST + //------------------------------------------------------------ + + //------------------------------------------------------------ + // -- RELATED TO EXCEL + public static function getExcelDetail(Request $request) { + $cols = ['code', 'apk_version_code', 'apk_version_name', 'last_connected_at', + 'company_name', 'address', 'street_address', 'notes', + 'col1', 'col2', 'col3', 'col4', 'col5', + 'col6', 'col7', 'col8', 'col9', 'col10', + 'is_active']; + return JSONResponse::Success(['rows' => TV::validateAndGetEloquentFromRequest($request)->select(...$cols)->get()]); + } + + // ---- RELATED TO EXPORT IMPORT + const EXCEL_SUCCESS = 'success'; + const EXCEL_FAILED = 'failed'; + const EXCEL_NO_CHANGE = 'no_change'; + const EXCEL_UPDATE = 'update'; + const EXCEL_TEMPLATE_COLS = ['code', 'company_name', 'address', 'street_address', 'notes', + 'col1', 'col2', 'col3', 'col4', 'col5', + 'col6', 'col7', 'col8', 'col9', 'col10', + 'is_active']; + public static function getExcelTemplate() { + $row = []; + foreach(self::EXCEL_TEMPLATE_COLS as $col) { $row[$col] = ''; } + $rows = [$row]; + return JSONResponse::Success(['rows' => $rows]); + } + public static function getExportData(Request $request) { + $rows = TV::validateAndGetEloquentFromRequest($request)->select(...self::EXCEL_TEMPLATE_COLS)->get(); + return JSONResponse::Success(['rows' => $rows]); + } + // ------ RELATED TO IMPORT + private static function _checkExcelFormat($tvRows) { + if(!$tvRows) throw new \Exception("No Data"); + + $firstTvRow = $tvRows[0]; + if(!$firstTvRow) throw new \Exception('Column not found'); + + // check if excel is empty or not + $firstTvRow['row'] = null; + if(!array_filter($firstTvRow)) throw new \Exception("Excel is empty"); + + // check is header col is exists + $errors = []; + foreach(self::EXCEL_TEMPLATE_COLS as $col) { + if(!array_key_exists($col, $firstTvRow)) $errors[$col] = "Column $col is Required"; + } + if($errors) throw ValidationException::withMessages($errors); + } + private static function _getCountTvCodes($tvRows, $tvCodes) { + if(!$tvCodes) $tvCodes = array_column($tvRows, 'code'); + $tvCodes = array_map(function($code) { return DatabaseHelper::trimUpperNull($code) ?? ''; }, $tvCodes); + return array_count_values($tvCodes); + } + private static function _changeModelFromTvRow(Tv $tv, $tvRow) { + $tv->company_name = $tvRow['company_name']; + $tv->address = $tvRow['address']; + $tv->street_address = $tvRow['street_address']; + $tv->code = $tvRow['code']; + $tv->col1 = $tvRow['col1']; + $tv->col2 = $tvRow['col2']; + $tv->col3 = $tvRow['col3']; + $tv->col4 = $tvRow['col4']; + $tv->col5 = $tvRow['col5']; + $tv->col6 = $tvRow['col6']; + $tv->col7 = $tvRow['col7']; + $tv->col8 = $tvRow['col8']; + $tv->col9 = $tvRow['col9']; + $tv->col10 = $tvRow['col10']; + $tv->notes = $tvRow['notes']; + return $tv; + } + public static function validateExcel($tvRows, $tvCodes = []) { + self::_checkExcelFormat($tvRows); + $countTvCodes = self::_getCountTvCodes($tvRows, $tvCodes); + + $endStatus = self::EXCEL_SUCCESS; + $results = []; + foreach($tvRows as $tvRow) { + $status = self::EXCEL_NO_CHANGE; + $message = ''; + $tvRow['code'] = strtoupper($tvRow['code'] ?? ''); + + try { + // STEP 1: check validation + $validator = Validator::make($tvRow, [ + 'row' => 'required|integer', + 'code' => 'required|string', + 'company_name' => 'nullable|string', + 'address' => 'nullable|string', + 'street_address' => 'nullable|string', + 'col1' => 'nullable|string', + 'col2' => 'nullable|string', + 'col3' => 'nullable|string', + 'col4' => 'nullable|string', + 'col5' => 'nullable|string', + 'col6' => 'nullable|string', + 'col7' => 'nullable|string', + 'col8' => 'nullable|string', + 'col9' => 'nullable|string', + 'col10' => 'nullable|string', + 'notes' => 'nullable|string', + ]); + if($validator->fails()) { + $errors = $validator->errors()->toArray(); + $messages = []; + foreach($errors as $eMessages) { $messages = array_merge($messages, $eMessages); } + throw new \Exception(implode(', ', $messages)); + } + + // STEP 2: check code is duplicate or not or not + $code = $tvRow['code']; + if(($countTvCodes[$code] ?? 0) > 1) throw new \Exception('Code is Duplicate in Excel'); + + // STEP 3: check code existing in database or not + $tvCheck = Tv::where('code', 'ilike', $code)->first(); + if(!$tvCheck) throw new \Exception("TV Code '$code' not found in database"); + + // STEP 4: check has update data or not + $tvCheck = self::_changeModelFromTvRow($tvCheck, $tvRow); + if($tvCheck->isDirty()) { + $status = self::EXCEL_UPDATE; + $message = DatabaseHelper::compileDirtyEloquentToArrMessage($tvCheck); + } + } catch (\Throwable $th) { + $endStatus = self::EXCEL_FAILED; + $status = self::EXCEL_FAILED; + $message = $th->getMessage(); + } + $results[] = ['row' => $tvRow['row'], 'status' => $status, 'message' => $message]; + } + return ['status' => $endStatus, 'results' => $results]; + } + public static function uploadExcel($tvRows, $oValidation) { + $validationResults = $oValidation['results']; + $countUploads = [ + self::EXCEL_UPDATE => 0, self::EXCEL_NO_CHANGE => 0, self::EXCEL_FAILED => 0 + ]; + $additionalErrors = []; + + foreach($validationResults as $result) { + $validateStatus = $result['status']; + try { + switch($validateStatus) { + case self::EXCEL_FAILED: + throw new \Exception($result['message']); + break; + + case self::EXCEL_UPDATE: + // get sparepart data + $idxTvRow = array_search($result['row'], array_column($tvRows, 'row')); + if($idxTvRow === false) throw new \Exception('Row Not Found'); + $tvRow = $tvRows[$idxTvRow]; + $tvRow['code'] = strtoupper($tvRow['code'] ?? ''); + + // try to upsert + DB::beginTransaction(); + $tv = TV::where('code', 'ilike', $tvRow['code'])->firstOrFail(); + $oldTV = $tv->replicate(); + $newTv = self::_changeModelFromTvRow($tv, $tvRow); + if(!$newTv->isDirty()) throw new \Exception('No Change'); + $newTv->save(); + + // save data log + TvLog::saveHistory(TvLog::TYPE_CREATE, $newTv->id, $oldTV, $newTv); + DB::commit(); + break; + + case self::EXCEL_NO_CHANGE: + default: + break; + } + } catch (\Throwable $th) { + DB::rollBack(); + $validateStatus = self::EXCEL_FAILED; + $additionalErrors[] = 'row ' . ($result['row'] ?? '-') . ' => ' . $th->getMessage(); + } + + $countUploads[$validateStatus]++; + } + + return ['countUploads' => $countUploads, 'additionalErrors' => $additionalErrors]; + } + // ------ END RELATED TO IMPORT + // ---- END RELATED TO EXPORT IMPORT + // -- END RELATED TO EXCEL //------------------------------------------------------------ } diff --git a/app/Models/TvLog.php b/app/Models/TvLog.php new file mode 100644 index 0000000..7fb78bf --- /dev/null +++ b/app/Models/TvLog.php @@ -0,0 +1,33 @@ +'object', 'to'=>'object']; + + const TYPE_CREATE = 'create'; + const TYPE_UPDATE = 'update'; + const TYPES = ['create', 'update']; + public static function saveHistory(String $type, int $tvFk, ?Tv $oldTv, ?Tv $newTv) { + if(!in_array($type, self::TYPES)) throw new \Exception("Type '$type' No Valid"); + $tvLog = new TvLog(); + $tvLog->tv_fk = $tvFk; + $tvLog->type = $type; + if($oldTv) { + $oldTv = $oldTv->toArray(); + $tvLog->from =$oldTv; + } + if($newTv) { + $newTv = $newTv->toArray(); + $tvLog->from =$newTv; + } + $tvLog->save(); + } +} diff --git a/database/migrations/2024_05_18_033105_tv.php b/database/migrations/2024_05_18_033105_tv.php index 31cbfcb..be94026 100644 --- a/database/migrations/2024_05_18_033105_tv.php +++ b/database/migrations/2024_05_18_033105_tv.php @@ -46,6 +46,15 @@ return new class extends Migration { $table->timestampsTz(); }); + Schema::create('tv_logs', function (Blueprint $table) { + $table->id(); + $table->foreignId('tv_fk'); + $table->string('type'); + $table->json('from')->nullable(); + $table->json('to')->nullable(); + $table->timestampsTz(); + }); + // Schema::create('tv_sessions', function (Blueprint $table) { // $table->id(); // $table->foreignId('tv_fk')->index(); @@ -89,6 +98,7 @@ return new class extends Migration { // Schema::drop('tv_app_logs'); // Schema::drop('tv_app_infos'); // Schema::drop('tv_sessions'); + Schema::drop('tv_logs'); Schema::drop('new_tv_requests'); Schema::drop('tvs'); } diff --git a/routes/api/superadmin.php b/routes/api/superadmin.php index c9c9565..4421764 100644 --- a/routes/api/superadmin.php +++ b/routes/api/superadmin.php @@ -35,6 +35,7 @@ Route::controller(TvController::class)->group(function() { Route::post('/tv/tv', 'init'); Route::post('/tv/tv/update', 'update'); Route::post('/tv/tv/change-status', 'changeStatus'); + Route::post('/tv/tv/excel', 'excel'); }); Route::controller(NewTvRequestController::class)->group(function() {