feat: tv with import export excel

v2.0.0
ricky rx 1 year ago
parent b82fb64dd3
commit 56384c1fa9

@ -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;
}
}
?>

@ -0,0 +1,48 @@
<?php
namespace App\Helper\Frontend;
use App\Helper\JSONResponse;
use App\Models\Tv;
use Illuminate\Http\Request;
/**
* Note:
* if any of frontend component need specific api query / data specific.
* Please add function to get data here.
*/
class ApiUtilities {
// for components/app/tv/button/excel.vue
public static function tvExcel(Request $request) {
$request->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');
}
}
?>

@ -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); }
}

@ -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'
], [

@ -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
//------------------------------------------------------------
}

@ -0,0 +1,33 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class TvLog extends Model {
use HasFactory;
protected $table = 'tv_logs';
protected $hidden = ['ik_cust_id', 'ik_address_id'];
protected $casts = ['from'=>'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();
}
}

@ -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');
}

@ -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() {

Loading…
Cancel
Save