Compare commits
10 Commits
a0ce09cdda
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cd60b836b5 | |||
| 85279d4616 | |||
| ff47f3f932 | |||
| 994608327a | |||
| cbca01a8e2 | |||
| 0b591da182 | |||
| dfa517f3a1 | |||
| bb18878336 | |||
| 32229a1b52 | |||
| 583ea3b9a0 |
102
app/Http/Controllers/MaintenanceHistoryController.php
Normal file
102
app/Http/Controllers/MaintenanceHistoryController.php
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Auth;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use App\Responses;
|
||||||
|
use App\Models\UserLogs;
|
||||||
|
use Validator;
|
||||||
|
|
||||||
|
class MaintenanceHistoryController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* API Get data history maintenance
|
||||||
|
*/
|
||||||
|
public function api_get_data_maintenance($vehicleid)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$data = DB::table('t_vehicles_maintenance_history as h')
|
||||||
|
->leftJoin('t_maintenance_vhc_type as t', function ($join) {
|
||||||
|
$join->whereRaw('FIND_IN_SET(t.id, h.idservice)');
|
||||||
|
})
|
||||||
|
->select(
|
||||||
|
'h.*',
|
||||||
|
DB::raw('GROUP_CONCAT(t.service_type SEPARATOR ", ") as service_names')
|
||||||
|
)
|
||||||
|
->where('h.vhc_id', $vehicleid)
|
||||||
|
->groupBy('h.id')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$apiResp = Responses::success("success list maintenance vehicles");
|
||||||
|
$apiResp["data"] = $data;
|
||||||
|
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$apiResp = Responses::error($e->getMessage());
|
||||||
|
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Update data history maintenance
|
||||||
|
*/
|
||||||
|
public function api_update_data_maintenance(Request $req, $vehicleid, $id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
$input = [
|
||||||
|
"idservice" => $req->idservice,
|
||||||
|
"odometer" => $req->odometer,
|
||||||
|
"dates" => $req->dates,
|
||||||
|
"ismaintenance"=> $req->ismaintenance
|
||||||
|
];
|
||||||
|
$rulesInput = [
|
||||||
|
"idservice" => "required|string",
|
||||||
|
"odometer" => "required|numeric",
|
||||||
|
"dates" => "required|string",
|
||||||
|
"ismaintenance" => "required|numeric"
|
||||||
|
];
|
||||||
|
|
||||||
|
// validasi input
|
||||||
|
$isValidInput = Validator::make($input, $rulesInput);
|
||||||
|
if (!$isValidInput->passes()) {
|
||||||
|
$apiResp = Responses::bad_input($isValidInput->messages()->first());
|
||||||
|
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$update = [
|
||||||
|
"idservice" => $req->idservice,
|
||||||
|
"vhc_id" => $vehicleid,
|
||||||
|
"odometer" => $req->odometer,
|
||||||
|
"dates" => $req->dates,
|
||||||
|
"ismaintenance" => $req->ismaintenance,
|
||||||
|
// "created_at" => date("Y-m-d H:i:s", $now),
|
||||||
|
// "udt" => date("Y-m-d H:i:s", $now),
|
||||||
|
// "created_by" => Auth::user()->id,
|
||||||
|
// "uby" => Auth::user()->id
|
||||||
|
];
|
||||||
|
DB::table('t_vehicles_maintenance_history')->where('id', $id)->update($update);
|
||||||
|
$apiResp = Responses::created("success update maintenance vehicles");
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
|
||||||
|
$log = [
|
||||||
|
"module" => "Vehicle Type",
|
||||||
|
"action" => "Create",
|
||||||
|
"desc" => "Add new vehicle maintenance type: " . $req->service_type,
|
||||||
|
];
|
||||||
|
UserLogs::insert(Auth::user()->id, $log);
|
||||||
|
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
$apiResp = Responses::error($e->getMessage());
|
||||||
|
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -63,12 +63,24 @@ class ReportsController extends Controller
|
|||||||
// $to_date = $req->input('to_date');
|
// $to_date = $req->input('to_date');
|
||||||
$from_date = $req->input('from_date') - Helper::TIMEFIX;
|
$from_date = $req->input('from_date') - Helper::TIMEFIX;
|
||||||
$to_date = $req->input('to_date') - Helper::TIMEFIX;
|
$to_date = $req->input('to_date') - Helper::TIMEFIX;
|
||||||
$vid = $req->input('vid');
|
|
||||||
|
// vid sekarang bisa berupa multiple value (dari select2 multiple di FE)
|
||||||
|
// bisa datang sebagai array (vid[]=1&vid[]=2) atau string comma-separated ("1,2,3")
|
||||||
|
$vidInput = $req->input('vid');
|
||||||
$vt = $req->input('vt');
|
$vt = $req->input('vt');
|
||||||
$poolcode = $req->input('poolcode');
|
$poolcode = $req->input('poolcode');
|
||||||
// $from_date = 1756054800;
|
// $from_date = 1756054800;
|
||||||
// $to_date = 1756745940;
|
// $to_date = 1756745940;
|
||||||
|
|
||||||
|
// Normalisasi $vid jadi array of int, buang value kosong (mis. opsi "-- All --")
|
||||||
|
$vidArr = [];
|
||||||
|
if (is_array($vidInput)) {
|
||||||
|
$vidArr = array_filter($vidInput, fn($v) => $v !== null && $v !== '');
|
||||||
|
} elseif (!empty($vidInput)) {
|
||||||
|
$vidArr = array_filter(explode(',', $vidInput), fn($v) => $v !== '');
|
||||||
|
}
|
||||||
|
$vidArr = array_values($vidArr);
|
||||||
|
|
||||||
// get month year
|
// get month year
|
||||||
$date = Carbon::createFromTimestamp($from_date);
|
$date = Carbon::createFromTimestamp($from_date);
|
||||||
$yymm = $date->format('ym');
|
$yymm = $date->format('ym');
|
||||||
@ -166,57 +178,70 @@ class ReportsController extends Controller
|
|||||||
// and if(? , vt.id = ? , 1=1)
|
// and if(? , vt.id = ? , 1=1)
|
||||||
// and if(? , t.pool_code = ? , 1=1)
|
// and if(? , t.pool_code = ? , 1=1)
|
||||||
// ", [$from_date, $to_date, $from_date, $to_date, $vid, $vid, $vt, $vt, $poolcode, $poolcode]);
|
// ", [$from_date, $to_date, $from_date, $to_date, $vid, $vid, $vt, $vt, $poolcode, $poolcode]);
|
||||||
|
|
||||||
|
// Bangun klausa IN untuk vid secara dinamis (jumlah placeholder = jumlah item terpilih)
|
||||||
|
// kalau kosong -> tidak difilter (1=1)
|
||||||
|
if (count($vidArr) > 0) {
|
||||||
|
$vidPlaceholders = implode(',', array_fill(0, count($vidArr), '?'));
|
||||||
|
$vidCondition = "t.id IN ($vidPlaceholders)";
|
||||||
|
} else {
|
||||||
|
$vidCondition = "1=1";
|
||||||
|
}
|
||||||
|
|
||||||
$sql =
|
$sql =
|
||||||
"WITH TotalMileage AS (
|
"WITH TotalMileage AS (
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
SUM(mileage) AS total_mileage,
|
|
||||||
COUNT(*) AS total_trip
|
|
||||||
FROM trips
|
|
||||||
WHERE start BETWEEN ? AND ?
|
|
||||||
GROUP BY id
|
|
||||||
)
|
|
||||||
SELECT
|
SELECT
|
||||||
t.*,
|
id,
|
||||||
ttll.start_latitude,
|
SUM(mileage) AS total_mileage,
|
||||||
ttll.start_longitude,
|
COUNT(*) AS total_trip
|
||||||
ttll.finish_latitude,
|
FROM trips
|
||||||
ttll.finish_longitude,
|
WHERE start BETWEEN ? AND ?
|
||||||
tm.total_mileage,
|
GROUP BY id
|
||||||
tm.total_trip,
|
)
|
||||||
vt.name AS type_name,
|
SELECT
|
||||||
ROW_NUMBER() OVER (
|
t.*,
|
||||||
PARTITION BY t.id
|
tm.total_mileage,
|
||||||
ORDER BY t.start
|
tm.total_trip,
|
||||||
) AS trip_id
|
vt.name AS type_name,
|
||||||
FROM trips t
|
ROW_NUMBER() OVER (
|
||||||
JOIN TotalMileage tm ON t.id = tm.id
|
PARTITION BY t.id
|
||||||
LEFT JOIN t_vehicles tvt ON tvt.id = t.vhc_id
|
ORDER BY t.start
|
||||||
LEFT JOIN t_vehicles_cats vt ON vt.id = tvt.cat_id
|
) AS trip_id
|
||||||
LEFT JOIN t_trips_latitude_longitude ttll ON t.id = ttll.tripsid
|
FROM trips t
|
||||||
WHERE
|
JOIN TotalMileage tm ON t.id = tm.id
|
||||||
t.start BETWEEN ? AND ?
|
LEFT JOIN t_vehicles tvt ON tvt.id = t.vhc_id
|
||||||
AND (? IS NULL OR t.id = ?)
|
LEFT JOIN t_vehicles_cats vt ON vt.id = tvt.cat_id
|
||||||
AND (? IS NULL OR vt.id = ?)
|
WHERE
|
||||||
AND (? IS NULL OR t.pool_code = ?);
|
t.start BETWEEN ? AND ?
|
||||||
";
|
AND {$vidCondition}
|
||||||
|
AND (? IS NULL OR vt.id = ?)
|
||||||
|
AND (? IS NULL OR t.pool_code = ?);
|
||||||
|
";
|
||||||
|
|
||||||
$vid = $vid ?: null;
|
|
||||||
$vt = $vt ?: null;
|
$vt = $vt ?: null;
|
||||||
$poolcode = $poolcode ?: null;
|
$poolcode = $poolcode ?: null;
|
||||||
|
|
||||||
$list = DB::select($sql, [
|
$bindings = [
|
||||||
$from_date,
|
$from_date,
|
||||||
$to_date,
|
$to_date,
|
||||||
$from_date,
|
$from_date,
|
||||||
$to_date,
|
$to_date,
|
||||||
$vid,
|
];
|
||||||
$vid,
|
|
||||||
|
// masukkan binding untuk IN(...) hanya kalau ada isinya
|
||||||
|
if (count($vidArr) > 0) {
|
||||||
|
$bindings = array_merge($bindings, $vidArr);
|
||||||
|
}
|
||||||
|
|
||||||
|
$bindings = array_merge($bindings, [
|
||||||
$vt,
|
$vt,
|
||||||
$vt,
|
$vt,
|
||||||
$poolcode,
|
$poolcode,
|
||||||
$poolcode,
|
$poolcode,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$list = DB::select($sql, $bindings);
|
||||||
|
|
||||||
// // RETURN 1 - LIST
|
// // RETURN 1 - LIST
|
||||||
// if($req->type != 'report'){
|
// if($req->type != 'report'){
|
||||||
$apiResp = Responses::success("success list vehicles report");
|
$apiResp = Responses::success("success list vehicles report");
|
||||||
|
|||||||
241
app/Http/Controllers/TripsLabelingController.php
Normal file
241
app/Http/Controllers/TripsLabelingController.php
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\UserLogs;
|
||||||
|
use Auth;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Responses;
|
||||||
|
use Validator;
|
||||||
|
|
||||||
|
class TripsLabelingController extends Controller
|
||||||
|
{
|
||||||
|
public function api_show_trips_label(Request $req, $id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$data = DB::table('t_sites_label')
|
||||||
|
->where('id', $id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$data) {
|
||||||
|
$apiResp = Responses::error("Sites labeling not found");
|
||||||
|
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiResp = [
|
||||||
|
"meta" => [
|
||||||
|
"code" => 200,
|
||||||
|
"type" => "success",
|
||||||
|
"message" => "success",
|
||||||
|
],
|
||||||
|
"data" => $data,
|
||||||
|
];
|
||||||
|
|
||||||
|
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$apiResp = Responses::error($e->getMessage());
|
||||||
|
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* View
|
||||||
|
*/
|
||||||
|
|
||||||
|
public function view_trips_label(Request $req)
|
||||||
|
{
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
$log = [
|
||||||
|
"module" => "Trips Label",
|
||||||
|
"action" => "View",
|
||||||
|
"desc" => "Open Trips Label",
|
||||||
|
];
|
||||||
|
UserLogs::insert(Auth::user()->id, $log);
|
||||||
|
return view("menu_v1.configs.trips_label", $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API
|
||||||
|
*/
|
||||||
|
public function api_list_trips_label(Request $req)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$datas = DB::table('t_sites_label')
|
||||||
|
->where('isdeleted', 0)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($datas as $key => $row) {
|
||||||
|
$datas[$key]->DT_RowIndex = $key + 1;
|
||||||
|
$datas[$key]->action = "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiResp = Responses::success("success list sites labeling");
|
||||||
|
$apiResp["count"] = count($datas);
|
||||||
|
$apiResp["data"] = $datas;
|
||||||
|
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$apiResp = Responses::error($e->getMessage());
|
||||||
|
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for creating a new resource.
|
||||||
|
*
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
public function api_add_trips_label(Request $req)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
$input = [
|
||||||
|
"address" => $req->address,
|
||||||
|
"lat" => $req->lat,
|
||||||
|
"lng" => $req->lng,
|
||||||
|
"radius" => $req->radius
|
||||||
|
];
|
||||||
|
$rulesInput = [
|
||||||
|
"address" => "required|string",
|
||||||
|
"lat" => "required|numeric|between:-90,90",
|
||||||
|
"lng" => "required|numeric|between:-180,180",
|
||||||
|
"radius" => "required|integer|min:0",
|
||||||
|
];
|
||||||
|
|
||||||
|
// validasi input
|
||||||
|
$isValidInput = Validator::make($input, $rulesInput);
|
||||||
|
if (!$isValidInput->passes()) {
|
||||||
|
$apiResp = Responses::bad_input($isValidInput->messages()->first());
|
||||||
|
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$insert = [
|
||||||
|
"address" => $req->address,
|
||||||
|
"lat" => $req->lat,
|
||||||
|
"lng" => $req->lng,
|
||||||
|
"radius" => $req->radius,
|
||||||
|
// "created_at" => date("Y-m-d H:i:s", $now),
|
||||||
|
// "udt" => date("Y-m-d H:i:s", $now),
|
||||||
|
// "created_by" => Auth::user()->id,
|
||||||
|
// "uby" => Auth::user()->id
|
||||||
|
];
|
||||||
|
DB::table('t_sites_label')->insert($insert);
|
||||||
|
$apiResp = Responses::created("success add new trips label");
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
|
||||||
|
$log = [
|
||||||
|
"module" => "Trips Label",
|
||||||
|
"action" => "Create",
|
||||||
|
"desc" => "Add new trips label: " . $req->address,
|
||||||
|
];
|
||||||
|
UserLogs::insert(Auth::user()->id, $log);
|
||||||
|
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
$apiResp = Responses::error($e->getMessage());
|
||||||
|
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*
|
||||||
|
* @param Request $req
|
||||||
|
* @param int $id
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
public function api_edit_trips_label(Request $req, $id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
$input = [
|
||||||
|
"address" => $req->address,
|
||||||
|
"lat" => $req->lat,
|
||||||
|
"lng" => $req->lng,
|
||||||
|
"radius" => $req->radius
|
||||||
|
];
|
||||||
|
$rulesInput = [
|
||||||
|
"address" => "required|string",
|
||||||
|
"lat" => "required|numeric|between:-90,90",
|
||||||
|
"lng" => "required|numeric|between:-180,180",
|
||||||
|
"radius" => "required|integer|min:0",
|
||||||
|
];
|
||||||
|
|
||||||
|
// validasi input
|
||||||
|
$isValidInput = Validator::make($input, $rulesInput);
|
||||||
|
if (!$isValidInput->passes()) {
|
||||||
|
$apiResp = Responses::bad_input($isValidInput->messages()->first());
|
||||||
|
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
$update = [
|
||||||
|
"address" => $req->address,
|
||||||
|
"lat" => $req->lat,
|
||||||
|
"lng" => $req->lng,
|
||||||
|
"radius" => $req->radius,
|
||||||
|
];
|
||||||
|
DB::table('t_sites_label')->where('id', $id)->update($update);
|
||||||
|
|
||||||
|
$apiResp = Responses::created("success update trips label");
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
$log = [
|
||||||
|
"module" => "Trips Label",
|
||||||
|
"action" => "Update",
|
||||||
|
"desc" => "Update trips label: " . $req->address,
|
||||||
|
];
|
||||||
|
UserLogs::insert(Auth::user()->id, $log);
|
||||||
|
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
$apiResp = Responses::error($e->getMessage());
|
||||||
|
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*
|
||||||
|
* @param int $id
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
public function api_del_trips_label($id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
DB::table('t_sites_label')->where('id', $id)->update([
|
||||||
|
'isdeleted' => 1,
|
||||||
|
// 'ddt' => date("Y-m-d H:i:s", time()),
|
||||||
|
// 'dby' => Auth::user()->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
$apiResp = Responses::success("success delete trips label");
|
||||||
|
|
||||||
|
$log = [
|
||||||
|
"module" => "Trips Label",
|
||||||
|
"action" => "Delete",
|
||||||
|
"desc" => "Delete trips label: " . $id,
|
||||||
|
];
|
||||||
|
UserLogs::insert(Auth::user()->id, $log);
|
||||||
|
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
$apiResp = Responses::error($e->getMessage());
|
||||||
|
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,7 +20,7 @@ class VehicleMaintenanceController extends Controller
|
|||||||
->first();
|
->first();
|
||||||
|
|
||||||
if (!$data) {
|
if (!$data) {
|
||||||
$apiResp = Responses::error("Truck type not found", 404);
|
$apiResp = Responses::error("Vehicle Maintenance type not found");
|
||||||
return new Response($apiResp, $apiResp["meta"]["code"]);
|
return new Response($apiResp, $apiResp["meta"]["code"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
656
resources/views/menu_v1/configs/trips_label.blade.php
Executable file
656
resources/views/menu_v1/configs/trips_label.blade.php
Executable file
@ -0,0 +1,656 @@
|
|||||||
|
@extends('app.app')
|
||||||
|
|
||||||
|
@section('title')
|
||||||
|
Trips Label
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('customcss')
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="content">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="row d-flex align-items-center">
|
||||||
|
<div class="col-3">
|
||||||
|
{{-- <p class="card-title text-bold mb-0">Vehicle Types (<span id="count_trucktypes">0</span>)</p> --}}
|
||||||
|
<p class="card-title text-bold mb-0">Trips Label</p>
|
||||||
|
</div>
|
||||||
|
@can('config_truck_type.create')
|
||||||
|
<div class="col text-end">
|
||||||
|
<button id="btnMdlNewTruckType" class="btn btn-sm btn-danger">Add New Trips Label</button>
|
||||||
|
</div>
|
||||||
|
@endcan
|
||||||
|
|
||||||
|
<div class="col-auto text-end ps-0">
|
||||||
|
{{-- <button class="btn btn-sm btn-danger">Upload</button> --}}
|
||||||
|
{{-- <button class="btn btn-sm btn-danger">Download</button> --}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table id="tTruckTypes" class="table table-hover dataTable">
|
||||||
|
<thead>
|
||||||
|
<tr class="">
|
||||||
|
<th class="">#</th>
|
||||||
|
<th class="text-center">Action</th>
|
||||||
|
<th class="w-100">Address</th>
|
||||||
|
<th class="w-100">Latitude</th>
|
||||||
|
<th class="w-100">Longitude</th>
|
||||||
|
<th class="w-100">Radius (m)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" id="mdlNewTruckType"
|
||||||
|
aria-labelledby="mdlNewTruckTypeLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog modal-dialog-centered modal-dialog-scrollable modal-xl">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="mdlNewTruckTypeLabel">Add New Trips Label</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form>
|
||||||
|
{{-- Trips Label --}}
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="border-bottom">
|
||||||
|
<h6>Maintenance Trips Label</h6>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-6 col-md-6">
|
||||||
|
<label for="address" class="col-form-label">Address:</label>
|
||||||
|
<input type="text" name="address" id="address" class="form-control"
|
||||||
|
placeholder="Please consider use english language">
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6 col-md-6">
|
||||||
|
<label for="lat" class="col-form-label">Latitude:</label>
|
||||||
|
<input type="number" name="lat" id="lat" class="form-control" step="any"
|
||||||
|
min="-90" max="90" placeholder="e.g. -6.234567">
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6 col-md-6">
|
||||||
|
<label for="lng" class="col-form-label">Longitude:</label>
|
||||||
|
<input type="number" name="lng" id="lng" class="form-control" step="any"
|
||||||
|
min="-180" max="180" placeholder="e.g. 106.987654">
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6 col-md-6">
|
||||||
|
<label for="radius" class="col-form-label">Radius (m):</label>
|
||||||
|
<input type="number" name="radius" id="radius" class="form-control" step="1"
|
||||||
|
min="0" placeholder="e.g. 100">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
<button id="btnSubmitNewTruckType" type="button" class="btn btn-sm btn-danger">Submit data</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" id="mdlEdtTruckType"
|
||||||
|
aria-labelledby="mdlEdtTruckTypeLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog modal-dialog-centered modal-dialog-scrollable modal-xl">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="mdlEdtTruckTypeLabel">Edit Trips Label</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form>
|
||||||
|
{{-- Truck Type --}}
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="border-bottom">
|
||||||
|
<h6>Trips Label</h6>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-6 col-md-6">
|
||||||
|
<label for="address_edit" class="col-form-label">Address:</label>
|
||||||
|
<input type="text" name="address" id="address_edit" class="form-control"
|
||||||
|
placeholder="Please consider use english language">
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6 col-md-6">
|
||||||
|
<label for="lat_edit" class="col-form-label">Latitude:</label>
|
||||||
|
<input type="number" name="lat" id="lat_edit" class="form-control"
|
||||||
|
step="any" min="-90" max="90" placeholder="e.g. -6.234567">
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6 col-md-6">
|
||||||
|
<label for="lng_edit" class="col-form-label">Longitude:</label>
|
||||||
|
<input type="number" name="lng" id="lng_edit" class="form-control"
|
||||||
|
step="any" min="-180" max="180" placeholder="e.g. 106.987654">
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6 col-md-6">
|
||||||
|
<label for="radius_edit" class="col-form-label">Radius (m):</label>
|
||||||
|
<input type="number" name="radius" id="radius_edit" class="form-control"
|
||||||
|
step="1" min="0" placeholder="e.g. 100">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
@can('config_truck_type.delete')
|
||||||
|
<button type="button" id="btnDelTruckType_updt" class="btn btn-sm btn-warning">Delete ?</button>
|
||||||
|
@endcan
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
@can('config_truck_type.edit')
|
||||||
|
<button id="btnSubmitEdtTruckType" type="button" class="btn btn-sm btn-danger">Update data</button>
|
||||||
|
@endcan
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" id="mdlDelTruckType"
|
||||||
|
aria-labelledby="mdlDelTruckTypeLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-sm">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="mdlDelTruckTypeLabel">Delete Trips Label</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<p class="mb-0">
|
||||||
|
Are you sure want to delete this trips label
|
||||||
|
<a href="#" class="text-danger">
|
||||||
|
<span id="del-type_name"></span>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-sm btn-danger" data-bs-dismiss="modal">Close</button>
|
||||||
|
<button id="btnSubmitDelTruckType" type="button" class="btn btn-sm btn-secondary">Yes,
|
||||||
|
delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('customjs')
|
||||||
|
<script src="{{ asset('assets/js/load-image.all.min.js') }}"></script>
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
const State = {
|
||||||
|
file_jimp_worker: "{{ asset('assets/js/worker/jimp.js') }}",
|
||||||
|
storage_lara: "{{ asset('storage') }}/",
|
||||||
|
stts_conf: {
|
||||||
|
active: "{{ App\Models\ConfTruckTypes::IS_ACTIVE }}",
|
||||||
|
inactive: "{{ App\Models\ConfTruckTypes::IS_INACTIVE }}",
|
||||||
|
publish: "{{ App\Models\ConfTruckTypes::IS_PUBLISH }}",
|
||||||
|
unpublish: "{{ App\Models\ConfTruckTypes::IS_UNPUBLISH }}",
|
||||||
|
},
|
||||||
|
delay_typing_front: 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Wrapper = {
|
||||||
|
activate: function() {
|
||||||
|
Wrapper.event();
|
||||||
|
DTable.activate();
|
||||||
|
RNew.activate();
|
||||||
|
RUpdt.activate();
|
||||||
|
RDel.activate();
|
||||||
|
},
|
||||||
|
event: function() {
|
||||||
|
$('#add-status').bind('change', function() {
|
||||||
|
if ($(this).is(':checked'))
|
||||||
|
$('#add-txtStatus').html('Active')
|
||||||
|
else
|
||||||
|
$('#add-txtStatus').html('Inactive')
|
||||||
|
});
|
||||||
|
$('#updt-status').bind('change', function() {
|
||||||
|
if ($(this).is(':checked'))
|
||||||
|
$('#updt-txtStatus').html('Active')
|
||||||
|
else
|
||||||
|
$('#updt-txtStatus').html('Inactive')
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#add-publish').bind('change', function() {
|
||||||
|
if ($(this).is(':checked'))
|
||||||
|
$('#add-txtPublish').html('Publish')
|
||||||
|
else
|
||||||
|
$('#add-txtPublish').html('Unpublish')
|
||||||
|
});
|
||||||
|
$('#updt-publish').bind('change', function() {
|
||||||
|
if ($(this).is(':checked'))
|
||||||
|
$('#updt-txtPublish').html('Publish')
|
||||||
|
else
|
||||||
|
$('#updt-txtPublish').html('Unpublish')
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const DTable = {
|
||||||
|
activate: function() {
|
||||||
|
DTable.reload();
|
||||||
|
},
|
||||||
|
reload: function() {
|
||||||
|
// $('#tTruckTypes').DataTable();
|
||||||
|
// if (Driver.Table.firstInitDataTable == 1) { loadTableSkeletonLoading() } else { Driver.Table.firstInitDataTable = 1; }
|
||||||
|
$('#tTruckTypes').DataTable({
|
||||||
|
processing: true,
|
||||||
|
serverSide: false,
|
||||||
|
bLengthChange: true,
|
||||||
|
deferRender: true,
|
||||||
|
destroy: true,
|
||||||
|
ajax: {
|
||||||
|
url: "{{ route('api_list_trips_label') }}",
|
||||||
|
type: 'GET',
|
||||||
|
complete: function(jqXHR, textStatus, c) {
|
||||||
|
let count = jqXHR.responseJSON.count;
|
||||||
|
if (typeof count != 'undefined') {
|
||||||
|
$('#count_trucktypes').text(count);
|
||||||
|
}
|
||||||
|
// removeTableSkeletonLoading()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deferRender: true,
|
||||||
|
columns: [{
|
||||||
|
data: 'DT_RowIndex',
|
||||||
|
className: 'text-end',
|
||||||
|
visible: true,
|
||||||
|
orderable: true,
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'action',
|
||||||
|
className: 'text-center',
|
||||||
|
visible: true,
|
||||||
|
orderable: true,
|
||||||
|
searchable: true,
|
||||||
|
render: function(data, type, row, meta) {
|
||||||
|
let action = `
|
||||||
|
<a href="#" class="text-decoration-none me-1 btnUpdtTruckType">
|
||||||
|
<span class="icon ion-eye fz-16"></span>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
// <a href="#" class="text-decoration-none text-danger btnDelTruckType">
|
||||||
|
// <span class="icon ion-trash-b fz-16"></span>
|
||||||
|
// </a>
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'address',
|
||||||
|
className: 'text-left text-nowrap',
|
||||||
|
visible: true,
|
||||||
|
orderable: true,
|
||||||
|
searchable: true,
|
||||||
|
createdCell: function(td, cellData, rowData, row, col) {
|
||||||
|
$(td).attr('data-ttid', rowData.id);
|
||||||
|
$(td).attr('data-type_name', rowData.type_name);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'lat',
|
||||||
|
className: 'text-left text-nowrap',
|
||||||
|
visible: true,
|
||||||
|
orderable: true,
|
||||||
|
searchable: true,
|
||||||
|
createdCell: function(td, cellData, rowData, row, col) {
|
||||||
|
$(td).attr('data-ttid', rowData.id);
|
||||||
|
$(td).attr('data-type_name', rowData.type_name);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'lng',
|
||||||
|
className: 'text-left text-nowrap',
|
||||||
|
visible: true,
|
||||||
|
orderable: true,
|
||||||
|
searchable: true,
|
||||||
|
createdCell: function(td, cellData, rowData, row, col) {
|
||||||
|
$(td).attr('data-ttid', rowData.id);
|
||||||
|
$(td).attr('data-type_name', rowData.type_name);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'radius',
|
||||||
|
className: 'text-left text-nowrap',
|
||||||
|
visible: true,
|
||||||
|
orderable: true,
|
||||||
|
searchable: true,
|
||||||
|
createdCell: function(td, cellData, rowData, row, col) {
|
||||||
|
$(td).attr('data-ttid', rowData.id);
|
||||||
|
$(td).attr('data-type_name', rowData.type_name);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const RNew = {
|
||||||
|
activate: function() {
|
||||||
|
RNew.event();
|
||||||
|
},
|
||||||
|
event: function() {
|
||||||
|
// modal
|
||||||
|
$('#btnMdlNewTruckType').on('click', function() {
|
||||||
|
$('#mdlNewTruckType').modal('show');
|
||||||
|
});
|
||||||
|
$('#mdlNewTruckType').on('shown.bs.modal', function() {
|
||||||
|
// init select2
|
||||||
|
});
|
||||||
|
// button
|
||||||
|
$('#btnSubmitNewTruckType').on('click', function() {
|
||||||
|
let data = RNew.getData();
|
||||||
|
RNew.submitData(data);
|
||||||
|
});
|
||||||
|
// input
|
||||||
|
},
|
||||||
|
getData: function() {
|
||||||
|
let data = {};
|
||||||
|
|
||||||
|
data.address = $('#address').val();
|
||||||
|
data.lat = $('#lat').val();
|
||||||
|
data.lng = $('#lng').val();
|
||||||
|
data.radius = $('#radius').val();
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
submitData: async function(data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof $('#btnSubmitNewTruckType').attr('disabed') != 'undefined') {
|
||||||
|
resolve({
|
||||||
|
type: 'fail'
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$('#btnSubmitNewTruckType').attr('disabed', true);
|
||||||
|
$.ajax({
|
||||||
|
url: "{{ route('api_add_trips_label') }}",
|
||||||
|
method: 'POST',
|
||||||
|
crossDomain: true,
|
||||||
|
processData: true,
|
||||||
|
headers: {
|
||||||
|
'x-api-key': Helper.getCookie('_trtk'),
|
||||||
|
'x-csrf-token': $('meta[name="csrf-token"]').attr('content'),
|
||||||
|
},
|
||||||
|
data: data,
|
||||||
|
success: (data, textStatus, jqXHR) => {
|
||||||
|
$('#btnSubmitNewTruckType').removeAttr('disabed');
|
||||||
|
if (data.meta.type != 'success') {
|
||||||
|
resolve({
|
||||||
|
type: 'fail'
|
||||||
|
});
|
||||||
|
Helper.toast('Warning', 'just now', data.meta.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Helper.toast('Success', 'just now',
|
||||||
|
'success add new trips label');
|
||||||
|
$('#mdlNewTruckType').modal('hide');
|
||||||
|
DTable.reload();
|
||||||
|
resolve({
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (jqXHR, textStatus, error) => {
|
||||||
|
$('#btnSubmitNewTruckType').removeAttr('disabed');
|
||||||
|
if (jqXHR.status >= 500) {
|
||||||
|
Helper.toast('Error', 'just now', 'Please try again');
|
||||||
|
} else {
|
||||||
|
Helper.toast('Error', 'just now', jqXHR.responseJSON.meta
|
||||||
|
.message);
|
||||||
|
}
|
||||||
|
resolve({
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const RUpdt = {
|
||||||
|
activate: function() {
|
||||||
|
RUpdt.event();
|
||||||
|
},
|
||||||
|
event: function() {
|
||||||
|
// modal
|
||||||
|
$('#tTruckTypes').on('click', '.btnUpdtTruckType', async function(e) {
|
||||||
|
let ttid = $(e.target).closest('tr').find('td[data-ttid]').data('ttid');
|
||||||
|
let resp = await RUpdt.reqData({
|
||||||
|
ttid
|
||||||
|
});
|
||||||
|
if (resp.type != 'success') {
|
||||||
|
Helper.toast('trips label Not Found', 'just now', 'please try again');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RUpdt.passDataToView(resp.data);
|
||||||
|
});
|
||||||
|
$('#mdlEdtTruckType').on('shown.bs.modal', function() {
|
||||||
|
// init select2
|
||||||
|
});
|
||||||
|
// button
|
||||||
|
$('#btnSubmitEdtTruckType').on('click', function() {
|
||||||
|
let data = RUpdt.getData();
|
||||||
|
RUpdt.submitData(data);
|
||||||
|
});
|
||||||
|
// input
|
||||||
|
},
|
||||||
|
reqData: function(params) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
$.ajax({
|
||||||
|
url: "{{ route('api_show_trips_label', '') }}/" + params.ttid,
|
||||||
|
method: 'GET',
|
||||||
|
crossDomain: true,
|
||||||
|
processData: true,
|
||||||
|
headers: {
|
||||||
|
'x-api-key': Helper.getCookie('_trtk'),
|
||||||
|
},
|
||||||
|
data: params,
|
||||||
|
success: (data, textStatus, jqXHR) => {
|
||||||
|
if (data.meta.type != 'success') {
|
||||||
|
resolve({
|
||||||
|
type: 'fail'
|
||||||
|
});
|
||||||
|
Helper.toast('Warning', 'just now', data.meta.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
resolve({
|
||||||
|
type: 'success',
|
||||||
|
data: data.data
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (jqXHR, textStatus, error) => {
|
||||||
|
if (jqXHR.status >= 500) {
|
||||||
|
Helper.toast('Error', 'just now', 'please try again');
|
||||||
|
} else {
|
||||||
|
Helper.toast('Error', 'just now', jqXHR.responseJSON.meta
|
||||||
|
.message);
|
||||||
|
}
|
||||||
|
resolve({
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
},
|
||||||
|
passDataToView: async function(data) {
|
||||||
|
$('#address_edit').val(data.address);
|
||||||
|
$('#lat_edit').val(data.lat);
|
||||||
|
$('#lng_edit').val(data.lng);
|
||||||
|
$('#radius_edit').val(data.radius);
|
||||||
|
|
||||||
|
$('#mdlEdtTruckType').data('id', data.id);
|
||||||
|
$('#mdlEdtTruckType').modal('show');
|
||||||
|
},
|
||||||
|
getData: function() {
|
||||||
|
let data = {};
|
||||||
|
|
||||||
|
data.ttid = $('#mdlEdtTruckType').data('id');
|
||||||
|
data.address = $('#address_edit').val();
|
||||||
|
data.lat = $('#lat_edit').val();
|
||||||
|
data.lng = $('#lng_edit').val();
|
||||||
|
data.radius = $('#radius_edit').val();
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
submitData: async function(data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof $('#btnSubmitEdtTruckType').attr('disabed') != 'undefined') {
|
||||||
|
resolve({
|
||||||
|
type: 'fail'
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$('#btnSubmitEdtTruckType').attr('disabed', true);
|
||||||
|
$.ajax({
|
||||||
|
url: "{{ route('api_edit_trips_label', '') }}/" + data.ttid,
|
||||||
|
method: 'PUT',
|
||||||
|
crossDomain: true,
|
||||||
|
processData: true,
|
||||||
|
headers: {
|
||||||
|
'x-api-key': Helper.getCookie('_trtk'),
|
||||||
|
'x-csrf-token': $('meta[name="csrf-token"]').attr('content'),
|
||||||
|
},
|
||||||
|
data: data,
|
||||||
|
success: (data, textStatus, jqXHR) => {
|
||||||
|
$('#btnSubmitEdtTruckType').removeAttr('disabed');
|
||||||
|
if (data.meta.type != 'success') {
|
||||||
|
resolve({
|
||||||
|
type: 'fail'
|
||||||
|
});
|
||||||
|
Helper.toast('Warning', 'just now', data.meta.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Helper.toast('Success', 'just now',
|
||||||
|
'success update trips label');
|
||||||
|
$('#mdlEdtTruckType').modal('hide');
|
||||||
|
DTable.reload();
|
||||||
|
resolve({
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (jqXHR, textStatus, error) => {
|
||||||
|
$('#btnSubmitEdtTruckType').removeAttr('disabed');
|
||||||
|
if (jqXHR.status >= 500) {
|
||||||
|
Helper.toast('Error', 'just now', 'Please try again');
|
||||||
|
} else {
|
||||||
|
Helper.toast('Error', 'just now', jqXHR.responseJSON.meta
|
||||||
|
.message);
|
||||||
|
}
|
||||||
|
resolve({
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const RDel = {
|
||||||
|
activate: function() {
|
||||||
|
RDel.event();
|
||||||
|
},
|
||||||
|
event: function() {
|
||||||
|
// on table
|
||||||
|
$('#tTruckTypes').on('click', '.btnDelTruckType', function(e) {
|
||||||
|
let row = $(e.target).closest('tr');
|
||||||
|
let ttid = row.find('td[data-ttid]').data('ttid');
|
||||||
|
let type_name = row.find('td[data-type_name]').data('type_name');
|
||||||
|
RDel.passDataToView({
|
||||||
|
ttid,
|
||||||
|
type_name,
|
||||||
|
});
|
||||||
|
$('#mdlDelTruckType').data('id', ttid);
|
||||||
|
$('#mdlDelTruckType').modal('show');
|
||||||
|
});
|
||||||
|
$('#btnSubmitDelTruckType').on('click', function() {
|
||||||
|
let data = {
|
||||||
|
ttid: $('#mdlDelTruckType').data('id'),
|
||||||
|
};
|
||||||
|
RDel.submitData(data);
|
||||||
|
});
|
||||||
|
// on modal update
|
||||||
|
$('#btnDelTruckType_updt').on('click', function(e) {
|
||||||
|
let data = RUpdt.getData();
|
||||||
|
RDel.passDataToView({
|
||||||
|
ttid: data.ttid,
|
||||||
|
address: data.address,
|
||||||
|
lat: data.lat,
|
||||||
|
long: data.long,
|
||||||
|
radius: data.radius,
|
||||||
|
});
|
||||||
|
$('#mdlDelTruckType').data('id', data.ttid);
|
||||||
|
$('#mdlDelTruckType').modal('show');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
passDataToView: function(data) {
|
||||||
|
$('#del-type_name').text(data.address);
|
||||||
|
},
|
||||||
|
submitData: async function(data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof $('#btnSubmitDelTruckType').attr('disabed') != 'undefined') {
|
||||||
|
resolve({
|
||||||
|
type: 'fail'
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$('#btnSubmitDelTruckType').attr('disabed', true);
|
||||||
|
$.ajax({
|
||||||
|
url: "{{ route('api_del_trips_label', '') }}/" + data.ttid,
|
||||||
|
method: 'DELETE',
|
||||||
|
crossDomain: true,
|
||||||
|
processData: true,
|
||||||
|
headers: {
|
||||||
|
'x-api-key': Helper.getCookie('_trtk'),
|
||||||
|
'x-csrf-token': $('meta[name="csrf-token"]').attr('content'),
|
||||||
|
},
|
||||||
|
data: data,
|
||||||
|
success: (data, textStatus, jqXHR) => {
|
||||||
|
$('#btnSubmitDelTruckType').removeAttr('disabed');
|
||||||
|
if (data.meta.type != 'success') {
|
||||||
|
resolve({
|
||||||
|
type: 'fail'
|
||||||
|
});
|
||||||
|
Helper.toast('Warning', 'just now', data.meta.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Helper.toast('Success', 'just now', 'success delete trips label');
|
||||||
|
$('#mdlDelTruckType').modal('hide');
|
||||||
|
$('#mdlEdtTruckType').modal('hide');
|
||||||
|
DTable.reload();
|
||||||
|
resolve({
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (jqXHR, textStatus, error) => {
|
||||||
|
$('#btnSubmitDelTruckType').removeAttr('disabed');
|
||||||
|
if (jqXHR.status >= 500) {
|
||||||
|
Helper.toast('Error', 'just now', 'Please try again');
|
||||||
|
} else {
|
||||||
|
Helper.toast('Error', 'just now', jqXHR.responseJSON.meta
|
||||||
|
.message);
|
||||||
|
}
|
||||||
|
resolve({
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Wrapper.activate();
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
764
resources/views/menu_v1/reports/old_vehicle_trips.blade.php
Normal file
764
resources/views/menu_v1/reports/old_vehicle_trips.blade.php
Normal file
@ -0,0 +1,764 @@
|
|||||||
|
@php
|
||||||
|
$user_role = Auth::user()->role;
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@extends('app.app')
|
||||||
|
|
||||||
|
@section('title')
|
||||||
|
Vehicles Trip Report
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('customcss')
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||||
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
|
||||||
|
<style>
|
||||||
|
.geo-pending {
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="content">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="row d-flex align-items-center">
|
||||||
|
<div class="col-3">
|
||||||
|
<p class="card-title text-bold mb-0">Vehicle Trip Report</p>
|
||||||
|
</div>
|
||||||
|
@if ($user_role == \App\Models\Users::ROLE_VENDOR || $user_role == \App\Models\Users::ROLE_ADMIN)
|
||||||
|
@can('vehicle.create')
|
||||||
|
<!-- <div class="col text-end">
|
||||||
|
<button id="btnMdlNewVhc" class="btn btn-sm btn-danger">Add New Vehicle</button>
|
||||||
|
</div> -->
|
||||||
|
@endcan
|
||||||
|
|
||||||
|
{{-- <div class="col-auto text-end ps-0">
|
||||||
|
<button class="btn btn-sm btn-danger">Upload</button>
|
||||||
|
</div> --}}
|
||||||
|
@endif
|
||||||
|
<div class="col-auto text-end ps-0">
|
||||||
|
<!-- <button class="btn btn-sm btn-danger">Download</button> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- filter -->
|
||||||
|
<div class="row g-3 align-items-end">
|
||||||
|
|
||||||
|
<div class="col-6 col-md-3 col-lg-2">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label text-muted mb-1">From</label>
|
||||||
|
<input type="text" class="form-control" id="tgl0" value="{{ date('d-m-Y 00:00') }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-md-3 col-lg-2">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label text-muted mb-1">To</label>
|
||||||
|
<input type="text" class="form-control" id="tgl1" value="{{ date('d-m-Y 23:59') }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-md-3 col-lg-2">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label text-muted mb-1">License Plate</label>
|
||||||
|
<select name="license_plate" class="form-control" id="filterNopol" style="width:100%;"
|
||||||
|
multiple="multiple">
|
||||||
|
<option value="">-- All --</option>
|
||||||
|
@foreach ($listNopol as $nopol)
|
||||||
|
<option value="{{ $nopol->id }}">{{ $nopol->nopol1 }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-md-3 col-lg-2">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label text-muted mb-1">Vehicle Category</label>
|
||||||
|
<select name="vehicle_type" class="form-control" id="vehicleType">
|
||||||
|
<option value="">-- All --</option>
|
||||||
|
@foreach ($vehicletype as $vehicletypes)
|
||||||
|
<option value="{{ $vehicletypes->id }}">{{ $vehicletypes->name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 col-md-3 col-lg-2">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label text-muted mb-1">Pool</label>
|
||||||
|
<select name="poolcode" class="form-control" id="poolcode">
|
||||||
|
<option value="">-- All --</option>
|
||||||
|
@foreach ($listpool as $listpools)
|
||||||
|
<option value="{{ $listpools->pool_code }}">{{ $listpools->pool_name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-lg-2 d-flex gap-2 justify-content-lg-end">
|
||||||
|
<button class="btn btn-primary w-100 w-lg-auto" id="submitFilter">
|
||||||
|
<i class="fa fa-search me-1"></i> Submit
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger w-100 w-lg-auto" id="btnDownloadReport">
|
||||||
|
<i class="fa fa-download me-1"></i> Download Report
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive p-3">
|
||||||
|
<table id="tVehicleTrips" class="table table-hover dataTable w-100">
|
||||||
|
<thead>
|
||||||
|
<tr class="">
|
||||||
|
<th class="" hidden>Vehicle ID</th>
|
||||||
|
<th class="">License Plate Number</th>
|
||||||
|
<th class="">Dist. Cat.</th>
|
||||||
|
<th class="">Pool</th>
|
||||||
|
<th class="">Number of Trip</th>
|
||||||
|
<th class="">Total Mileage (km)</th>
|
||||||
|
<th class="">Trip #</th>
|
||||||
|
<th class="hidden">Date</th>
|
||||||
|
<th class="hidden">Hour Start</th>
|
||||||
|
<th class="hidden">Hour Finish</th>
|
||||||
|
<th class="">Start</th>
|
||||||
|
<th class="">Finish</th>
|
||||||
|
<th class="">Duration</th>
|
||||||
|
<th class="">Start (km)</th>
|
||||||
|
<th class="">Finish (km)</th>
|
||||||
|
<th class="">Distance (km)</th>
|
||||||
|
<th class="">Fuel (L)</th>
|
||||||
|
<th class="">Vehicle Category</th>
|
||||||
|
<th class="">Detail</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- modal -->
|
||||||
|
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" id="mdlDetailTrip"
|
||||||
|
aria-labelledby="mdlDetailTripLabel" aria-hidden="true">
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('customjs')
|
||||||
|
<script src="{{ asset('assets/js/load-image.all.min.js') }}"></script>
|
||||||
|
<!-- DataTables Buttons + JSZip (for Excel export) -->
|
||||||
|
<script src="https://cdn.datatables.net/buttons/2.4.2/js/dataTables.buttons.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
||||||
|
<script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.html5.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||||
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||||
|
<script src="https://unpkg.com/leaflet-routing-machine@3.2.12/dist/leaflet-routing-machine.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
|
<script src="{{ asset('assets/vendor/printThis.js') }}"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
const State = {
|
||||||
|
file_jimp_worker: "{{ asset('assets/js/worker/jimp.js') }}",
|
||||||
|
storage_lara: "{{ asset('storage') }}/",
|
||||||
|
dvc_type: {
|
||||||
|
built_in: "{{ \App\Models\Devices::TYPE_BUILT_IN }}",
|
||||||
|
portable: "{{ \App\Models\Devices::TYPE_PORTABLE }}",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Wrapper = {
|
||||||
|
activate: function() {
|
||||||
|
Wrapper.init();
|
||||||
|
Wrapper.event();
|
||||||
|
},
|
||||||
|
init: () => {
|
||||||
|
$('#tgl0, #tgl1').datetimepicker({
|
||||||
|
format: 'd-m-Y H:i',
|
||||||
|
defaultTime: '00:00',
|
||||||
|
closeOnDateSelect: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
event: function() {
|
||||||
|
$('#submitFilter').on('click', function() {
|
||||||
|
DTable.reload();
|
||||||
|
});
|
||||||
|
// Trigger export on external button click
|
||||||
|
$('#btnDownloadReport').on('click', function() {
|
||||||
|
DTable.table.button('.buttons-excel').trigger();
|
||||||
|
});
|
||||||
|
|
||||||
|
// detail trip
|
||||||
|
$('#tVehicleTrips').on('click', '.btnDetailTrip', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const vid = $(this).data('vid');
|
||||||
|
const tgl0 = $(this).data('tgl0');
|
||||||
|
const tgl1 = $(this).data('tgl1');
|
||||||
|
const nopol1 = $(this).data('nopol1');
|
||||||
|
|
||||||
|
$('#mdlDetailTrip').empty().load("{{ route('view_report_trip_detail') }}",
|
||||||
|
`vid=${vid}&tgl0=${tgl0}&tgl1=${tgl1}&nopol1=${nopol1}`, () => {
|
||||||
|
$('#mdlDetailTrip').modal('show')
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
||||||
|
$('#filterNopol').select2({});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// SITE LABELING (dari API, bukan hardcode)
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// Data referensi site (lat/lng dalam desimal) — diisi via loadSiteLocations()
|
||||||
|
let SITE_LOCATIONS = [];
|
||||||
|
|
||||||
|
// Ambil data site labeling dari API
|
||||||
|
function loadSiteLocations() {
|
||||||
|
return $.ajax({
|
||||||
|
url: "{{ route('api_list_trips_label') }}",
|
||||||
|
type: 'GET',
|
||||||
|
dataType: 'json',
|
||||||
|
}).then(function(json) {
|
||||||
|
const list = (json && json.data) ? json.data : [];
|
||||||
|
// hanya ambil yang belum dihapus (isdeleted = 0), lalu normalisasi field yang dipakai
|
||||||
|
SITE_LOCATIONS = list
|
||||||
|
.filter(site => Number(site.isdeleted) === 0)
|
||||||
|
.map(site => ({
|
||||||
|
id: site.id,
|
||||||
|
address: site.address,
|
||||||
|
lat: parseFloat(site.lat),
|
||||||
|
lng: parseFloat(site.lng),
|
||||||
|
// radius dari API sudah dalam meter
|
||||||
|
radius: parseFloat(site.radius),
|
||||||
|
}));
|
||||||
|
}).catch(function(err) {
|
||||||
|
console.error('Failed to load site locations:', err);
|
||||||
|
SITE_LOCATIONS = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper untuk parse string "lat, long" menjadi { lat, lng }
|
||||||
|
function parseLatLong(latLongStr) {
|
||||||
|
if (!latLongStr || typeof latLongStr !== 'string') {
|
||||||
|
return {
|
||||||
|
lat: null,
|
||||||
|
lng: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = latLongStr.split(',').map(part => parseFloat(part.trim()));
|
||||||
|
|
||||||
|
if (parts.length !== 2 || isNaN(parts[0]) || isNaN(parts[1])) {
|
||||||
|
return {
|
||||||
|
lat: null,
|
||||||
|
lng: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
lat: parts[0],
|
||||||
|
lng: parts[1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hitung jarak antara 2 koordinat (formula Haversine), hasil dalam meter
|
||||||
|
function getDistanceMeters(lat1, lon1, lat2, lon2) {
|
||||||
|
const R = 6371000; // radius bumi (meter)
|
||||||
|
const toRad = (deg) => deg * Math.PI / 180;
|
||||||
|
const dLat = toRad(lat2 - lat1);
|
||||||
|
const dLon = toRad(lon2 - lon1);
|
||||||
|
const a = Math.sin(dLat / 2) ** 2 +
|
||||||
|
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
|
||||||
|
Math.sin(dLon / 2) ** 2;
|
||||||
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
|
return R * c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cari site terdekat dari koordinat yang diberikan; null jika tidak ada yang cukup dekat
|
||||||
|
// menggunakan radius masing-masing site (bukan radius global)
|
||||||
|
function findNearestSite(lat, lng) {
|
||||||
|
if (lat === undefined || lng === undefined || lat === null || lng === null || lat === '' || lng === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let nearest = null;
|
||||||
|
let minDist = Infinity;
|
||||||
|
SITE_LOCATIONS.forEach(site => {
|
||||||
|
const dist = getDistanceMeters(parseFloat(lat), parseFloat(lng), site.lat, site.lng);
|
||||||
|
if (dist <= site.radius && dist < minDist) {
|
||||||
|
minDist = dist;
|
||||||
|
nearest = site;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return nearest;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format label sesuai output yang diinginkan
|
||||||
|
// address dari API sudah lengkap (mis. "Bitterlaun, Hera, Cristo Rei, Dili")
|
||||||
|
function formatSiteLabel(site) {
|
||||||
|
return `Site ${site.address}, Timor-Leste`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// REVERSE GEOCODING (Nominatim) - fallback ketika startLoc/finishLoc kosong
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// Cache in-memory supaya koordinat yang sama tidak fetch berulang
|
||||||
|
const geocodeCache = new Map();
|
||||||
|
|
||||||
|
// Antrian sederhana supaya request ke Nominatim tidak lebih dari 1/detik
|
||||||
|
let geocodeQueue = Promise.resolve();
|
||||||
|
|
||||||
|
function reverseGeocode(lat, lng) {
|
||||||
|
if (lat === null || lng === null || lat === undefined || lng === undefined || isNaN(lat) || isNaN(lng)) {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = `${lat.toFixed(5)},${lng.toFixed(5)}`;
|
||||||
|
if (geocodeCache.has(key)) {
|
||||||
|
return Promise.resolve(geocodeCache.get(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chain ke queue supaya jaraknya minimal ~1100ms antar request
|
||||||
|
geocodeQueue = geocodeQueue.then(() =>
|
||||||
|
new Promise(resolve => setTimeout(resolve, 1100))
|
||||||
|
);
|
||||||
|
|
||||||
|
return geocodeQueue.then(async () => {
|
||||||
|
try {
|
||||||
|
const url =
|
||||||
|
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&zoom=18&addressdetails=1`;
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'Accept-Language': 'id'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
const address = json && json.display_name ? json.display_name : null;
|
||||||
|
geocodeCache.set(key, address);
|
||||||
|
return address;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Reverse geocode gagal:', e);
|
||||||
|
geocodeCache.set(key, null);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bikin id unik untuk elemen placeholder lokasi per baris+kolom,
|
||||||
|
// dipakai untuk update teks langsung via DOM setelah geocode selesai di background.
|
||||||
|
function geoElementId(row, kind) {
|
||||||
|
// kind: 'start' | 'finish'
|
||||||
|
return `geo-${kind}-${row.id}-${row.trip_id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jalankan reverse geocode di BACKGROUND (tidak menunggu / tidak blocking render tabel).
|
||||||
|
// Tabel sudah tampil duluan dengan placeholder "Load for location...", lalu begitu
|
||||||
|
// Nominatim balas untuk satu titik, teks placeholder-nya langsung di-update di tempat
|
||||||
|
// (tanpa redraw seluruh tabel), row demi row sesuai antrian reverseGeocode.
|
||||||
|
function scheduleBackgroundGeocode(rows) {
|
||||||
|
if (!Array.isArray(rows)) return;
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
const {
|
||||||
|
lat: startLat,
|
||||||
|
lng: startLng
|
||||||
|
} = parseLatLong(row.start_lat_long);
|
||||||
|
const startNearestSite = findNearestSite(startLat, startLng);
|
||||||
|
|
||||||
|
// hanya perlu geocode kalau bukan site terdaftar & startLoc memang kosong
|
||||||
|
if (!startNearestSite && !row.startLoc) {
|
||||||
|
geocodeAndUpdateCell(geoElementId(row, 'start'), startLat, startLng);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
lat: finishLat,
|
||||||
|
lng: finishLng
|
||||||
|
} = parseLatLong(row.finish_lat_long);
|
||||||
|
const finishNearestSite = findNearestSite(finishLat, finishLng);
|
||||||
|
|
||||||
|
if (!finishNearestSite && !row.finishLoc) {
|
||||||
|
geocodeAndUpdateCell(geoElementId(row, 'finish'), finishLat, finishLng);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panggil reverseGeocode (sudah antri otomatis max 1 request/detik),
|
||||||
|
// begitu hasilnya datang, cari elemen placeholder di DOM lewat id-nya
|
||||||
|
// dan replace teksnya. Kalau elemen sudah tidak ada (misal tabel sudah
|
||||||
|
// di-reload/filter berubah), update di-skip secara aman.
|
||||||
|
function geocodeAndUpdateCell(elId, lat, lng) {
|
||||||
|
reverseGeocode(lat, lng).then(address => {
|
||||||
|
const $el = $('#' + elId);
|
||||||
|
if (!$el.length) return; // tabel sudah berganti, abaikan hasil basi ini
|
||||||
|
|
||||||
|
if (address) {
|
||||||
|
$el.text(Helper.shortenText(address, 255)).removeClass('geo-pending');
|
||||||
|
} else {
|
||||||
|
$el.text('-').removeClass('geo-pending');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// END SITE LABELING
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
const DTable = {
|
||||||
|
table: null,
|
||||||
|
lastAjax: null, // keep track of the last ajax request
|
||||||
|
activate: function() {
|
||||||
|
DTable.reload();
|
||||||
|
},
|
||||||
|
reload: function() {
|
||||||
|
// Abort the last request if it's still running
|
||||||
|
if (DTable.lastAjax) {
|
||||||
|
DTable.lastAjax.abort();
|
||||||
|
}
|
||||||
|
if (DTable.table) {
|
||||||
|
// If table already exists → reload
|
||||||
|
DTable.table.ajax.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DTable.table = $('#tVehicleTrips').DataTable({
|
||||||
|
searching: false, // 🔹 remove search box
|
||||||
|
ordering: false, // 🔹 disable sorting for all columns
|
||||||
|
processing: true,
|
||||||
|
serverSide: false,
|
||||||
|
bLengthChange: true,
|
||||||
|
deferRender: true,
|
||||||
|
destroy: true,
|
||||||
|
ajax: function(data, callback, settings) {
|
||||||
|
// Abort previous
|
||||||
|
if (DTable.lastAjax) {
|
||||||
|
DTable.lastAjax.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire new request
|
||||||
|
DTable.lastAjax = $.ajax({
|
||||||
|
url: `{{ route('api_report_vehicle_trips_list') }}?
|
||||||
|
cptid=${AppState.current_company}
|
||||||
|
&from_date=${moment($('#tgl0').val(), "DD-MM-YYYY HH:mm").unix()}
|
||||||
|
&to_date=${moment($('#tgl1').val(), "DD-MM-YYYY HH:mm").unix()}
|
||||||
|
&vid=${$('#filterNopol').val() || ''}
|
||||||
|
&vt=${$('#vehicleType').val() || ''}
|
||||||
|
&poolcode=${$('#poolcode').val() || ''}
|
||||||
|
`,
|
||||||
|
type: 'GET',
|
||||||
|
success: function(json) {
|
||||||
|
// Tampilkan tabel DULUAN dengan semua data apa adanya
|
||||||
|
// (baris yang startLoc/finishLoc kosong akan muncul
|
||||||
|
// sebagai placeholder "Load for location...").
|
||||||
|
callback(json);
|
||||||
|
|
||||||
|
// Reset antrian geocode supaya request dari load sebelumnya
|
||||||
|
// (misal filter lama) tidak numpuk / membuang kuota Nominatim.
|
||||||
|
geocodeQueue = Promise.resolve();
|
||||||
|
|
||||||
|
// Baru SETELAH itu, isi lokasi yang kosong di background.
|
||||||
|
// setTimeout 0 supaya DOM tabel sudah pasti ter-render dulu
|
||||||
|
// sebelum kita cari elemen placeholder-nya.
|
||||||
|
setTimeout(() => {
|
||||||
|
scheduleBackgroundGeocode(json && json.data);
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
if (status !== 'abort') {
|
||||||
|
console.error("AJAX error:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
DTable.lastAjax = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deferRender: true,
|
||||||
|
columns: [{
|
||||||
|
data: 'id',
|
||||||
|
visible: false
|
||||||
|
}, // vhc_id
|
||||||
|
{
|
||||||
|
data: 'nopol1',
|
||||||
|
className: 'text-start',
|
||||||
|
visible: true,
|
||||||
|
orderable: true,
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: "dc_code",
|
||||||
|
className: 'text-start'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: "pool_code",
|
||||||
|
className: 'text-start'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: "total_trip",
|
||||||
|
className: 'text-end'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: "total_mileage",
|
||||||
|
className: 'text-end',
|
||||||
|
render: (data, type, row, meta) => parseFloat(data).toFixed(2)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: "trip_id",
|
||||||
|
className: 'text-end'
|
||||||
|
},
|
||||||
|
// ==== KOLOM BARU: hanya untuk export, tidak tampil di layar ====
|
||||||
|
{
|
||||||
|
data: 'start',
|
||||||
|
visible: false,
|
||||||
|
render: (data, type, row, meta) => {
|
||||||
|
return moment.unix(data + AppState.TIMEFIX).format('DD MMM YYYY');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'start',
|
||||||
|
visible: false,
|
||||||
|
render: (data, type, row, meta) => {
|
||||||
|
return moment.unix(data + AppState.TIMEFIX).format('HH:mm:ss');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'finish',
|
||||||
|
visible: false,
|
||||||
|
render: (data, type, row, meta) => {
|
||||||
|
return moment.unix(data + AppState.TIMEFIX).format('HH:mm:ss');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// ==== END KOLOM BARU ====
|
||||||
|
{
|
||||||
|
data: 'start',
|
||||||
|
render: (data, type, row) => {
|
||||||
|
const {
|
||||||
|
lat: startLat,
|
||||||
|
lng: startLng
|
||||||
|
} = parseLatLong(row.start_lat_long);
|
||||||
|
const nearestSite = findNearestSite(startLat, startLng);
|
||||||
|
|
||||||
|
let locationLabel;
|
||||||
|
if (nearestSite) {
|
||||||
|
// Prioritas 1: site terdaftar
|
||||||
|
locationLabel = formatSiteLabel(nearestSite);
|
||||||
|
} else if (row.startLoc) {
|
||||||
|
// Prioritas 2: startLoc sudah ada dari API
|
||||||
|
locationLabel = Helper.shortenText(decodeURIComponent(row.startLoc), 255);
|
||||||
|
} else if (type === 'export') {
|
||||||
|
// Export bersifat sinkron, tidak bisa menunggu hasil geocode background
|
||||||
|
locationLabel = '-';
|
||||||
|
} else {
|
||||||
|
// Prioritas 3: kosong -> tampilkan placeholder dulu,
|
||||||
|
// nanti di-update otomatis oleh scheduleBackgroundGeocode()
|
||||||
|
// begitu hasil dari Nominatim datang.
|
||||||
|
locationLabel =
|
||||||
|
`<span id="${geoElementId(row, 'start')}" class="geo-pending">Load for location...</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'export') return locationLabel;
|
||||||
|
|
||||||
|
return `${moment.unix(data + AppState.TIMEFIX).format('DD MMM YYYY HH:mm:ss')}<br>${locationLabel}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'finish',
|
||||||
|
render: (data, type, row) => {
|
||||||
|
const {
|
||||||
|
lat: finishLat,
|
||||||
|
lng: finishLng
|
||||||
|
} = parseLatLong(row.finish_lat_long);
|
||||||
|
const nearestSite = findNearestSite(finishLat, finishLng);
|
||||||
|
|
||||||
|
let locationLabel;
|
||||||
|
if (nearestSite) {
|
||||||
|
locationLabel = formatSiteLabel(nearestSite);
|
||||||
|
} else if (row.finishLoc) {
|
||||||
|
locationLabel = Helper.shortenText(decodeURIComponent(row.finishLoc), 255);
|
||||||
|
} else if (type === 'export') {
|
||||||
|
locationLabel = '-';
|
||||||
|
} else {
|
||||||
|
locationLabel =
|
||||||
|
`<span id="${geoElementId(row, 'finish')}" class="geo-pending">Load for location...</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'export') return locationLabel;
|
||||||
|
|
||||||
|
return `${moment.unix(data + AppState.TIMEFIX).format('DD MMM YYYY HH:mm:ss')}<br>${locationLabel}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
className: 'text-start',
|
||||||
|
render: function(data, type, row, meta) {
|
||||||
|
const start = moment.unix(row.start);
|
||||||
|
const finish = moment.unix(row.finish);
|
||||||
|
|
||||||
|
const diff = moment.duration(finish.diff(start));
|
||||||
|
const hours = Math.floor(diff.asHours());
|
||||||
|
const minutes = diff.minutes();
|
||||||
|
|
||||||
|
return `${hours}h ${minutes}m`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'startMileage',
|
||||||
|
className: 'text-end',
|
||||||
|
render: function(data, type, row, meta) {
|
||||||
|
return (data === null) ? '0' : parseFloat(data).toFixed(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'finishMileage',
|
||||||
|
className: 'text-end',
|
||||||
|
render: function(data, type, row, meta) {
|
||||||
|
return (data === null) ? '0' : parseFloat(data).toFixed(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'mileage',
|
||||||
|
className: 'text-end',
|
||||||
|
render: function(data, type, row, meta) {
|
||||||
|
return (parseFloat(data).toFixed(2));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'fuel_consume',
|
||||||
|
className: 'text-end',
|
||||||
|
render: function(data, type, row, meta) {
|
||||||
|
return (parseFloat(data / 10).toFixed(1));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: "type_name",
|
||||||
|
className: 'text-start'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'vhc_id',
|
||||||
|
className: 'text-center',
|
||||||
|
render: function(data, type, row, meta) {
|
||||||
|
let action = `
|
||||||
|
<a href="#" class="text-decoration-none me-1 btnDetailTrip"
|
||||||
|
data-vid="${data}" data-tgl0="${row.start}" data-tgl1="${row.finish}" data-nopol1="${row.nopol1}">
|
||||||
|
<span class="icon ion-eye fz-16"></span>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
paging: false,
|
||||||
|
drawCallback: function(settings) {
|
||||||
|
const api = this.api();
|
||||||
|
const rows = api.rows({
|
||||||
|
page: 'current'
|
||||||
|
}).nodes();
|
||||||
|
let last = null;
|
||||||
|
|
||||||
|
api.column(0, {
|
||||||
|
page: 'current'
|
||||||
|
}) // vhc_id col
|
||||||
|
.data()
|
||||||
|
.each(function(group, i) {
|
||||||
|
if (last !== group) {
|
||||||
|
const rowspanCount = api
|
||||||
|
.column(0, {
|
||||||
|
page: 'current'
|
||||||
|
})
|
||||||
|
.data()
|
||||||
|
.filter(function(d) {
|
||||||
|
return d === group;
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
$(rows).eq(i).find('td:eq(0)').attr('rowspan', rowspanCount);
|
||||||
|
$(rows).eq(i).find('td:eq(1)').attr('rowspan', rowspanCount);
|
||||||
|
$(rows).eq(i).find('td:eq(2)').attr('rowspan', rowspanCount);
|
||||||
|
$(rows).eq(i).find('td:eq(3)').attr('rowspan', rowspanCount);
|
||||||
|
$(rows).eq(i).find('td:eq(4)').attr('rowspan', rowspanCount);
|
||||||
|
|
||||||
|
last = group;
|
||||||
|
} else {
|
||||||
|
$(rows).eq(i).find('td:eq(0)').remove();
|
||||||
|
$(rows).eq(i).find('td:eq(0)').remove();
|
||||||
|
$(rows).eq(i).find('td:eq(0)').remove();
|
||||||
|
$(rows).eq(i).find('td:eq(0)').remove();
|
||||||
|
$(rows).eq(i).find('td:eq(0)').remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
buttons: [{
|
||||||
|
extend: 'excelHtml5',
|
||||||
|
title: () => {
|
||||||
|
return `
|
||||||
|
Vehicle Trip Report -
|
||||||
|
${moment($('#tgl0').val(), "DD-MM-YYYY HH:mm").format('DD MMM YYYY HH:mm')}
|
||||||
|
to
|
||||||
|
${moment($('#tgl1').val(), "DD-MM-YYYY HH:mm").format('DD MMM YYYY HH:mm')}`
|
||||||
|
},
|
||||||
|
className: 'd-none', // hide default button
|
||||||
|
exportOptions: {
|
||||||
|
// index kolom baru (7,8,9 = Date, Hour Start, Hour Finish) sudah dimasukkan manual
|
||||||
|
// karena kolom tsb visible:false sehingga tidak tercover selector ':visible'
|
||||||
|
columns: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17],
|
||||||
|
orthogonal: 'export'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function safeVal(selector) {
|
||||||
|
const val = $(selector).val();
|
||||||
|
return (val === undefined || val === null || val === '') ? null : val;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function convertImgHtmlToFile(imgHtml, imgWidth, imgHeight, mimeType, fileName) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
let canvas = document.createElement("canvas")
|
||||||
|
|
||||||
|
// Draw image to canvas.
|
||||||
|
canvas.width = imgWidth
|
||||||
|
canvas.height = imgHeight
|
||||||
|
let ctx = canvas.getContext('2d')
|
||||||
|
ctx.drawImage(imgHtml, 0, 0)
|
||||||
|
|
||||||
|
// Convert canvas to ImageState.
|
||||||
|
let imageData = ctx.getImageData(0, 0, imgWidth, imgHeight)
|
||||||
|
|
||||||
|
// Convert canvas to Blob
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
let newReader = new FileReader();
|
||||||
|
newReader.addEventListener('loadend', () => {
|
||||||
|
// Convert canvas to ArrayBuffer
|
||||||
|
let arrayBuffer = newReader.result
|
||||||
|
|
||||||
|
// Generate as file
|
||||||
|
let newFile = new File([arrayBuffer], fileName, {
|
||||||
|
type: mimeType,
|
||||||
|
lastModified: new Date(),
|
||||||
|
size: arrayBuffer.byteLength,
|
||||||
|
})
|
||||||
|
resolve(newFile)
|
||||||
|
});
|
||||||
|
newReader.readAsArrayBuffer(blob);
|
||||||
|
}, mimeType);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e.message)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pastikan data site sudah ter-load SEBELUM tabel trip dirender,
|
||||||
|
// supaya findNearestSite() punya data yang benar saat kolom start/finish di-render.
|
||||||
|
loadSiteLocations().always(function() {
|
||||||
|
Wrapper.activate();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
@ -11,6 +11,12 @@
|
|||||||
@section('customcss')
|
@section('customcss')
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
|
||||||
|
<style>
|
||||||
|
.geo-pending {
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
@ -25,8 +31,8 @@
|
|||||||
@if ($user_role == \App\Models\Users::ROLE_VENDOR || $user_role == \App\Models\Users::ROLE_ADMIN)
|
@if ($user_role == \App\Models\Users::ROLE_VENDOR || $user_role == \App\Models\Users::ROLE_ADMIN)
|
||||||
@can('vehicle.create')
|
@can('vehicle.create')
|
||||||
<!-- <div class="col text-end">
|
<!-- <div class="col text-end">
|
||||||
<button id="btnMdlNewVhc" class="btn btn-sm btn-danger">Add New Vehicle</button>
|
<button id="btnMdlNewVhc" class="btn btn-sm btn-danger">Add New Vehicle</button>
|
||||||
</div> -->
|
</div> -->
|
||||||
@endcan
|
@endcan
|
||||||
|
|
||||||
{{-- <div class="col-auto text-end ps-0">
|
{{-- <div class="col-auto text-end ps-0">
|
||||||
@ -59,7 +65,8 @@
|
|||||||
<div class="col-6 col-md-3 col-lg-2">
|
<div class="col-6 col-md-3 col-lg-2">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label text-muted mb-1">License Plate</label>
|
<label class="form-label text-muted mb-1">License Plate</label>
|
||||||
<select name="license_plate" class="form-control" id="filterNopol">
|
<select name="license_plate" class="form-control" id="filterNopol" style="width:100%;"
|
||||||
|
multiple="multiple">
|
||||||
<option value="">-- All --</option>
|
<option value="">-- All --</option>
|
||||||
@foreach ($listNopol as $nopol)
|
@foreach ($listNopol as $nopol)
|
||||||
<option value="{{ $nopol->id }}">{{ $nopol->nopol1 }}</option>
|
<option value="{{ $nopol->id }}">{{ $nopol->nopol1 }}</option>
|
||||||
@ -128,74 +135,6 @@
|
|||||||
<th class="">Detail</th>
|
<th class="">Detail</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<!-- <tbody>
|
|
||||||
<tr>
|
|
||||||
<td>24-679</td>
|
|
||||||
<td>24-679</td>
|
|
||||||
<td>26</td>
|
|
||||||
<td>442.3</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>24-682</td>
|
|
||||||
<td>24-682</td>
|
|
||||||
<td>28</td>
|
|
||||||
<td>312.7</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>24-877</td>
|
|
||||||
<td>24-877</td>
|
|
||||||
<td>15</td>
|
|
||||||
<td>276.4</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>A.16-347</td>
|
|
||||||
<td>A.16-347</td>
|
|
||||||
<td>37</td>
|
|
||||||
<td>543.2</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>AL2025-B21</td>
|
|
||||||
<td>AL2025-B21</td>
|
|
||||||
<td>18</td>
|
|
||||||
<td>295.4</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>B.16-542</td>
|
|
||||||
<td>B.16-542</td>
|
|
||||||
<td>32</td>
|
|
||||||
<td>478.2</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>B.24-264</td>
|
|
||||||
<td>B.24-264</td>
|
|
||||||
<td>24</td>
|
|
||||||
<td>188.4</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>B.24-274</td>
|
|
||||||
<td>B.24-274</td>
|
|
||||||
<td>33</td>
|
|
||||||
<td>440.1</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Ford Ranger</td>
|
|
||||||
<td>26-199</td>
|
|
||||||
<td>30</td>
|
|
||||||
<td>631.4</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Nissan AD</td>
|
|
||||||
<td>B.10-517</td>
|
|
||||||
<td>27</td>
|
|
||||||
<td>375.9</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Triton</td>
|
|
||||||
<td>A.29-127</td>
|
|
||||||
<td>26</td>
|
|
||||||
<td>508.2</td>
|
|
||||||
</tr>
|
|
||||||
</tbody> -->
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -206,18 +145,6 @@
|
|||||||
<!-- modal -->
|
<!-- modal -->
|
||||||
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" id="mdlDetailTrip"
|
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" id="mdlDetailTrip"
|
||||||
aria-labelledby="mdlDetailTripLabel" aria-hidden="true">
|
aria-labelledby="mdlDetailTripLabel" aria-hidden="true">
|
||||||
<!-- <div class="modal-dialog modal-dialog modal-dialog-centered modal-dialog-scrollable modal-xl">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="mdlDetailTripLabel">-----</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@ -227,9 +154,6 @@
|
|||||||
<script src="https://cdn.datatables.net/buttons/2.4.2/js/dataTables.buttons.min.js"></script>
|
<script src="https://cdn.datatables.net/buttons/2.4.2/js/dataTables.buttons.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
||||||
<script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.html5.min.js"></script>
|
<script src="https://cdn.datatables.net/buttons/2.4.2/js/buttons.html5.min.js"></script>
|
||||||
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"
|
|
||||||
integrity="sha512-ozq8xQKq6urvuU6jNgkfqAmT7jKN2XumbrX1JiB3TnF7tI48DPI4Gy1GXKD/V3EExgAs1V+pRO7vwtS1LHg0Gw=="
|
|
||||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script> -->
|
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||||
<script src="https://unpkg.com/leaflet-routing-machine@3.2.12/dist/leaflet-routing-machine.js"></script>
|
<script src="https://unpkg.com/leaflet-routing-machine@3.2.12/dist/leaflet-routing-machine.js"></script>
|
||||||
@ -253,15 +177,12 @@
|
|||||||
activate: function() {
|
activate: function() {
|
||||||
Wrapper.init();
|
Wrapper.init();
|
||||||
Wrapper.event();
|
Wrapper.event();
|
||||||
// DTable.table = $('#tVehicleTrips').DataTable()
|
|
||||||
// DTable.activate();
|
|
||||||
},
|
},
|
||||||
init: () => {
|
init: () => {
|
||||||
$('#tgl0, #tgl1').datetimepicker({
|
$('#tgl0, #tgl1').datetimepicker({
|
||||||
format: 'd-m-Y H:i',
|
format: 'd-m-Y H:i',
|
||||||
defaultTime: '00:00',
|
defaultTime: '00:00',
|
||||||
closeOnDateSelect: true,
|
closeOnDateSelect: true,
|
||||||
// mask:true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
@ -282,44 +203,71 @@
|
|||||||
const tgl1 = $(this).data('tgl1');
|
const tgl1 = $(this).data('tgl1');
|
||||||
const nopol1 = $(this).data('nopol1');
|
const nopol1 = $(this).data('nopol1');
|
||||||
|
|
||||||
// $("#mdlDetailTrip").load("{{ route('view_report_trip_detail') }}");
|
|
||||||
$('#mdlDetailTrip').empty().load("{{ route('view_report_trip_detail') }}",
|
$('#mdlDetailTrip').empty().load("{{ route('view_report_trip_detail') }}",
|
||||||
`vid=${vid}&tgl0=${tgl0}&tgl1=${tgl1}&nopol1=${nopol1}`, () => {
|
`vid=${vid}&tgl0=${tgl0}&tgl1=${tgl1}&nopol1=${nopol1}`, () => {
|
||||||
$('#mdlDetailTrip').modal('show')
|
$('#mdlDetailTrip').modal('show')
|
||||||
})
|
})
|
||||||
|
|
||||||
});
|
});
|
||||||
|
$('#filterNopol').select2({});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
// Data referensi site (lat/lng dalam desimal, bukan koma)
|
|
||||||
const SITE_LOCATIONS = [{
|
|
||||||
name: 'Bitterlaun',
|
|
||||||
district: 'Dili',
|
|
||||||
subdistrict: 'Cristo Rei',
|
|
||||||
village: 'Hera',
|
|
||||||
lat: -8.548328,
|
|
||||||
lng: 125.623808
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Bebonuk',
|
|
||||||
district: 'Dili',
|
|
||||||
subdistrict: 'Dom Aleixo',
|
|
||||||
village: 'Comoro',
|
|
||||||
lat: -8.547527,
|
|
||||||
lng: 125.548280
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Balide',
|
|
||||||
district: 'Dili',
|
|
||||||
subdistrict: 'Nain Feto',
|
|
||||||
village: 'Santa Cruz',
|
|
||||||
lat: -8.564396,
|
|
||||||
lng: 125.582151
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Radius toleransi "berdekatan" dalam meter — sesuaikan sesuai kebutuhan
|
// ================================
|
||||||
const SITE_PROXIMITY_RADIUS_METERS = 500;
|
// SITE LABELING (dari API, bukan hardcode)
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// Data referensi site (lat/lng dalam desimal) — diisi via loadSiteLocations()
|
||||||
|
let SITE_LOCATIONS = [];
|
||||||
|
|
||||||
|
// Ambil data site labeling dari API
|
||||||
|
function loadSiteLocations() {
|
||||||
|
return $.ajax({
|
||||||
|
url: "{{ route('api_list_trips_label') }}",
|
||||||
|
type: 'GET',
|
||||||
|
dataType: 'json',
|
||||||
|
}).then(function(json) {
|
||||||
|
const list = (json && json.data) ? json.data : [];
|
||||||
|
// hanya ambil yang belum dihapus (isdeleted = 0), lalu normalisasi field yang dipakai
|
||||||
|
SITE_LOCATIONS = list
|
||||||
|
.filter(site => Number(site.isdeleted) === 0)
|
||||||
|
.map(site => ({
|
||||||
|
id: site.id,
|
||||||
|
address: site.address,
|
||||||
|
lat: parseFloat(site.lat),
|
||||||
|
lng: parseFloat(site.lng),
|
||||||
|
// radius dari API sudah dalam meter
|
||||||
|
radius: parseFloat(site.radius),
|
||||||
|
}));
|
||||||
|
}).catch(function(err) {
|
||||||
|
console.error('Failed to load site locations:', err);
|
||||||
|
SITE_LOCATIONS = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper untuk parse string "lat, long" menjadi { lat, lng }
|
||||||
|
function parseLatLong(latLongStr) {
|
||||||
|
if (!latLongStr || typeof latLongStr !== 'string') {
|
||||||
|
return {
|
||||||
|
lat: null,
|
||||||
|
lng: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = latLongStr.split(',').map(part => parseFloat(part.trim()));
|
||||||
|
|
||||||
|
if (parts.length !== 2 || isNaN(parts[0]) || isNaN(parts[1])) {
|
||||||
|
return {
|
||||||
|
lat: null,
|
||||||
|
lng: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
lat: parts[0],
|
||||||
|
lng: parts[1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Hitung jarak antara 2 koordinat (formula Haversine), hasil dalam meter
|
// Hitung jarak antara 2 koordinat (formula Haversine), hasil dalam meter
|
||||||
function getDistanceMeters(lat1, lon1, lat2, lon2) {
|
function getDistanceMeters(lat1, lon1, lat2, lon2) {
|
||||||
@ -335,6 +283,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cari site terdekat dari koordinat yang diberikan; null jika tidak ada yang cukup dekat
|
// Cari site terdekat dari koordinat yang diberikan; null jika tidak ada yang cukup dekat
|
||||||
|
// menggunakan radius masing-masing site (bukan radius global)
|
||||||
function findNearestSite(lat, lng) {
|
function findNearestSite(lat, lng) {
|
||||||
if (lat === undefined || lng === undefined || lat === null || lng === null || lat === '' || lng === '') {
|
if (lat === undefined || lng === undefined || lat === null || lng === null || lat === '' || lng === '') {
|
||||||
return null;
|
return null;
|
||||||
@ -343,7 +292,7 @@
|
|||||||
let minDist = Infinity;
|
let minDist = Infinity;
|
||||||
SITE_LOCATIONS.forEach(site => {
|
SITE_LOCATIONS.forEach(site => {
|
||||||
const dist = getDistanceMeters(parseFloat(lat), parseFloat(lng), site.lat, site.lng);
|
const dist = getDistanceMeters(parseFloat(lat), parseFloat(lng), site.lat, site.lng);
|
||||||
if (dist <= SITE_PROXIMITY_RADIUS_METERS && dist < minDist) {
|
if (dist <= site.radius && dist < minDist) {
|
||||||
minDist = dist;
|
minDist = dist;
|
||||||
nearest = site;
|
nearest = site;
|
||||||
}
|
}
|
||||||
@ -352,10 +301,212 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Format label sesuai output yang diinginkan
|
// Format label sesuai output yang diinginkan
|
||||||
|
// address dari API sudah lengkap (mis. "Bitterlaun, Hera, Cristo Rei, Dili")
|
||||||
function formatSiteLabel(site) {
|
function formatSiteLabel(site) {
|
||||||
return `Site ${site.name}, ${site.village}, ${site.subdistrict}, ${site.district}, Timor-Leste`;
|
return `Site ${site.address}, Timor-Leste`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// NORMALISASI KONTINUITAS TRIP // >>> ADDED
|
||||||
|
// ================================
|
||||||
|
// Menyamakan start (lokasi & odometer) suatu trip dengan finish trip
|
||||||
|
// sebelumnya (untuk kendaraan yang sama), jika keduanya tidak match.
|
||||||
|
// Timestamp 'start' TIDAK diubah karena itu waktu kejadian sebenarnya —
|
||||||
|
// yang disamakan hanya titik lokasi & odometer supaya laporan terlihat
|
||||||
|
// kontinu (finish trip N == start trip N+1).
|
||||||
|
//
|
||||||
|
// PENTING: perbandingan koordinat dilakukan secara NUMERIK (parseFloat +
|
||||||
|
// hitung jarak meter via getDistanceMeters), BUKAN string mentah.
|
||||||
|
// Ini supaya kasus seperti berikut tetap terdeteksi & diperbaiki:
|
||||||
|
// - km/odometer sudah nyambung (finish trip N == start trip N+1)
|
||||||
|
// TAPI titik GPS-nya beda beberapa meter (drift GPS), sehingga
|
||||||
|
// salah satu titik "masuk" radius site tertentu (mis. "Bank BNU")
|
||||||
|
// dan yang lain tidak -> teks lokasi jadi kelihatan "loncat"
|
||||||
|
// walau kendaraan sebenarnya di tempat yang sama.
|
||||||
|
// Toleransi 0 meter berarti: kalau titiknya tidak PERSIS sama,
|
||||||
|
// selalu disamakan dengan finish sebelumnya. Ini paling aman untuk
|
||||||
|
// laporan (finish trip N akan selalu == start trip N+1).
|
||||||
|
const TRIP_CONTINUITY_TOLERANCE_METERS = 0; // >>> ADDED
|
||||||
|
|
||||||
|
function normalizeTripContinuity(rows) { // >>> ADDED
|
||||||
|
if (!Array.isArray(rows) || rows.length === 0) return rows;
|
||||||
|
|
||||||
|
// Group per kendaraan (pakai field 'id' = vhc_id)
|
||||||
|
const groups = {};
|
||||||
|
rows.forEach(row => {
|
||||||
|
if (!groups[row.id]) groups[row.id] = [];
|
||||||
|
groups[row.id].push(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.values(groups).forEach(groupRows => {
|
||||||
|
// Urutkan berdasarkan trip_id (fallback ke start time) supaya urutan
|
||||||
|
// kronologis benar sebelum membandingkan prev/curr.
|
||||||
|
groupRows.sort((a, b) => (a.trip_id ?? a.start) - (b.trip_id ?? b.start));
|
||||||
|
|
||||||
|
for (let i = 1; i < groupRows.length; i++) {
|
||||||
|
const prev = groupRows[i - 1];
|
||||||
|
const curr = groupRows[i];
|
||||||
|
|
||||||
|
// 1) Cek mismatch km/odometer
|
||||||
|
const kmMismatch = Number(prev.finishMileage) !== Number(curr.startMileage);
|
||||||
|
|
||||||
|
// 2) Cek mismatch koordinat GPS -- HANYA kalau datanya tersedia,
|
||||||
|
// karena banyak data (seperti contoh Anda) punya
|
||||||
|
// start_lat_long/finish_lat_long = null untuk semua baris.
|
||||||
|
const prevCoord = parseLatLong(prev.finish_lat_long);
|
||||||
|
const currCoord = parseLatLong(curr.start_lat_long);
|
||||||
|
let coordMismatch = false;
|
||||||
|
if (prevCoord.lat !== null && currCoord.lat !== null) {
|
||||||
|
const distMeters = getDistanceMeters(
|
||||||
|
prevCoord.lat, prevCoord.lng,
|
||||||
|
currCoord.lat, currCoord.lng
|
||||||
|
);
|
||||||
|
coordMismatch = distMeters > TRIP_CONTINUITY_TOLERANCE_METERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Cek mismatch TEKS lokasi (startLoc vs finishLoc). Ini penting
|
||||||
|
// karena saat lat_long null, satu-satunya sumber lokasi yang
|
||||||
|
// dipakai untuk render adalah field teks ini (lihat kolom
|
||||||
|
// Start/Finish -> prioritas 2: row.startLoc / row.finishLoc).
|
||||||
|
// Tanpa cek ini, kasus seperti "Estrada Suai - Maliana, Cendana"
|
||||||
|
// vs "Bank BNU, Estrada Suai - Maliana, Cendana" tidak akan
|
||||||
|
// terdeteksi sebagai mismatch walau km-nya sudah nyambung.
|
||||||
|
const prevFinishText = (prev.finishLoc || '').trim();
|
||||||
|
const currStartText = (curr.startLoc || '').trim();
|
||||||
|
const textMismatch = prevFinishText !== currStartText;
|
||||||
|
|
||||||
|
if (kmMismatch || coordMismatch || textMismatch) {
|
||||||
|
curr.start_lat_long = prev.finish_lat_long;
|
||||||
|
curr.startLoc = prev.finishLoc;
|
||||||
|
curr.startMileage = prev.finishMileage;
|
||||||
|
|
||||||
|
// Hitung ulang Distance (kolom 'mileage') dari selisih odometer
|
||||||
|
// yang baru (finishMileage - startMileage yang sudah disinkronkan),
|
||||||
|
// supaya kolom Distance tetap konsisten dengan kolom Start(km)/Finish(km)
|
||||||
|
// yang tampil di tabel. Sebelumnya 'mileage' berasal dari GPS track
|
||||||
|
// mentah, yang bisa beda dengan selisih odometer setelah normalisasi ini.
|
||||||
|
//
|
||||||
|
// - Kalau hasilnya minus (finishMileage < startMileage, misal odometer
|
||||||
|
// sempat reset/kebalik), jadikan positif (absolute value).
|
||||||
|
// - Lalu bulatkan ke bilangan bulat terdekat: desimal >= 0.5 dibulatkan
|
||||||
|
// ke atas, desimal < 0.5 dibulatkan ke bawah (standard round-half-up;
|
||||||
|
// Math.round sudah berlaku demikian untuk angka positif).
|
||||||
|
const rawDiff = Number(curr.finishMileage) - Number(curr.startMileage);
|
||||||
|
if (!isNaN(rawDiff)) {
|
||||||
|
curr.mileage = Math.round(Math.abs(rawDiff));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
} // >>> ADDED (end normalizeTripContinuity)
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// REVERSE GEOCODING (Nominatim) - fallback ketika startLoc/finishLoc kosong
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// Cache in-memory supaya koordinat yang sama tidak fetch berulang
|
||||||
|
const geocodeCache = new Map();
|
||||||
|
|
||||||
|
// Antrian sederhana supaya request ke Nominatim tidak lebih dari 1/detik
|
||||||
|
let geocodeQueue = Promise.resolve();
|
||||||
|
|
||||||
|
function reverseGeocode(lat, lng) {
|
||||||
|
if (lat === null || lng === null || lat === undefined || lng === undefined || isNaN(lat) || isNaN(lng)) {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = `${lat.toFixed(5)},${lng.toFixed(5)}`;
|
||||||
|
if (geocodeCache.has(key)) {
|
||||||
|
return Promise.resolve(geocodeCache.get(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chain ke queue supaya jaraknya minimal ~1100ms antar request
|
||||||
|
geocodeQueue = geocodeQueue.then(() =>
|
||||||
|
new Promise(resolve => setTimeout(resolve, 1100))
|
||||||
|
);
|
||||||
|
|
||||||
|
return geocodeQueue.then(async () => {
|
||||||
|
try {
|
||||||
|
const url =
|
||||||
|
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&zoom=18&addressdetails=1`;
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'Accept-Language': 'id'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
const address = json && json.display_name ? json.display_name : null;
|
||||||
|
geocodeCache.set(key, address);
|
||||||
|
return address;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Reverse geocode gagal:', e);
|
||||||
|
geocodeCache.set(key, null);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bikin id unik untuk elemen placeholder lokasi per baris+kolom,
|
||||||
|
// dipakai untuk update teks langsung via DOM setelah geocode selesai di background.
|
||||||
|
function geoElementId(row, kind) {
|
||||||
|
// kind: 'start' | 'finish'
|
||||||
|
return `geo-${kind}-${row.id}-${row.trip_id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jalankan reverse geocode di BACKGROUND (tidak menunggu / tidak blocking render tabel).
|
||||||
|
// Tabel sudah tampil duluan dengan placeholder "Load for location...", lalu begitu
|
||||||
|
// Nominatim balas untuk satu titik, teks placeholder-nya langsung di-update di tempat
|
||||||
|
// (tanpa redraw seluruh tabel), row demi row sesuai antrian reverseGeocode.
|
||||||
|
function scheduleBackgroundGeocode(rows) {
|
||||||
|
if (!Array.isArray(rows)) return;
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
const {
|
||||||
|
lat: startLat,
|
||||||
|
lng: startLng
|
||||||
|
} = parseLatLong(row.start_lat_long);
|
||||||
|
const startNearestSite = findNearestSite(startLat, startLng);
|
||||||
|
|
||||||
|
// hanya perlu geocode kalau bukan site terdaftar & startLoc memang kosong
|
||||||
|
if (!startNearestSite && !row.startLoc) {
|
||||||
|
geocodeAndUpdateCell(geoElementId(row, 'start'), startLat, startLng);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
lat: finishLat,
|
||||||
|
lng: finishLng
|
||||||
|
} = parseLatLong(row.finish_lat_long);
|
||||||
|
const finishNearestSite = findNearestSite(finishLat, finishLng);
|
||||||
|
|
||||||
|
if (!finishNearestSite && !row.finishLoc) {
|
||||||
|
geocodeAndUpdateCell(geoElementId(row, 'finish'), finishLat, finishLng);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panggil reverseGeocode (sudah antri otomatis max 1 request/detik),
|
||||||
|
// begitu hasilnya datang, cari elemen placeholder di DOM lewat id-nya
|
||||||
|
// dan replace teksnya. Kalau elemen sudah tidak ada (misal tabel sudah
|
||||||
|
// di-reload/filter berubah), update di-skip secara aman.
|
||||||
|
function geocodeAndUpdateCell(elId, lat, lng) {
|
||||||
|
reverseGeocode(lat, lng).then(address => {
|
||||||
|
const $el = $('#' + elId);
|
||||||
|
if (!$el.length) return; // tabel sudah berganti, abaikan hasil basi ini
|
||||||
|
|
||||||
|
if (address) {
|
||||||
|
$el.text(Helper.shortenText(address, 255)).removeClass('geo-pending');
|
||||||
|
} else {
|
||||||
|
$el.text('-').removeClass('geo-pending');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// END SITE LABELING
|
||||||
|
// ================================
|
||||||
|
|
||||||
const DTable = {
|
const DTable = {
|
||||||
table: null,
|
table: null,
|
||||||
lastAjax: null, // keep track of the last ajax request
|
lastAjax: null, // keep track of the last ajax request
|
||||||
@ -399,7 +550,28 @@
|
|||||||
`,
|
`,
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
success: function(json) {
|
success: function(json) {
|
||||||
|
// >>> ADDED: normalisasi kontinuitas start/finish antar trip
|
||||||
|
// (per kendaraan) SEBELUM data dirender ke tabel.
|
||||||
|
if (json && Array.isArray(json.data)) {
|
||||||
|
json.data = normalizeTripContinuity(json.data);
|
||||||
|
}
|
||||||
|
// >>> END ADDED
|
||||||
|
|
||||||
|
// Tampilkan tabel DULUAN dengan semua data apa adanya
|
||||||
|
// (baris yang startLoc/finishLoc kosong akan muncul
|
||||||
|
// sebagai placeholder "Load for location...").
|
||||||
callback(json);
|
callback(json);
|
||||||
|
|
||||||
|
// Reset antrian geocode supaya request dari load sebelumnya
|
||||||
|
// (misal filter lama) tidak numpuk / membuang kuota Nominatim.
|
||||||
|
geocodeQueue = Promise.resolve();
|
||||||
|
|
||||||
|
// Baru SETELAH itu, isi lokasi yang kosong di background.
|
||||||
|
// setTimeout 0 supaya DOM tabel sudah pasti ter-render dulu
|
||||||
|
// sebelum kita cari elemen placeholder-nya.
|
||||||
|
setTimeout(() => {
|
||||||
|
scheduleBackgroundGeocode(json && json.data);
|
||||||
|
}, 0);
|
||||||
},
|
},
|
||||||
error: function(xhr, status, error) {
|
error: function(xhr, status, error) {
|
||||||
if (status !== 'abort') {
|
if (status !== 'abort') {
|
||||||
@ -469,96 +641,62 @@
|
|||||||
// ==== END KOLOM BARU ====
|
// ==== END KOLOM BARU ====
|
||||||
{
|
{
|
||||||
data: 'start',
|
data: 'start',
|
||||||
render: (data, type, row, meta) => {
|
render: (data, type, row) => {
|
||||||
// saat export, hanya tampilkan lokasi tanpa tanggal/jam
|
const {
|
||||||
if (type === 'export') {
|
lat: startLat,
|
||||||
const nearestSite = findNearestSite(
|
lng: startLng
|
||||||
row.start_latitude,
|
} = parseLatLong(row.start_lat_long);
|
||||||
row.start_longitude
|
const nearestSite = findNearestSite(startLat, startLng);
|
||||||
);
|
|
||||||
|
|
||||||
if (nearestSite) {
|
|
||||||
return formatSiteLabel(nearestSite);
|
|
||||||
}
|
|
||||||
return Helper.shortenText(decodeURIComponent(row.startLoc ||
|
|
||||||
'address'), 255) || "-";
|
|
||||||
}
|
|
||||||
const nearestSite = findNearestSite(
|
|
||||||
row.start_latitude,
|
|
||||||
row.start_longitude
|
|
||||||
);
|
|
||||||
|
|
||||||
|
let locationLabel;
|
||||||
if (nearestSite) {
|
if (nearestSite) {
|
||||||
return formatSiteLabel(nearestSite);
|
// Prioritas 1: site terdaftar
|
||||||
|
locationLabel = formatSiteLabel(nearestSite);
|
||||||
|
} else if (row.startLoc) {
|
||||||
|
// Prioritas 2: startLoc sudah ada dari API
|
||||||
|
locationLabel = Helper.shortenText(decodeURIComponent(row
|
||||||
|
.startLoc), 255);
|
||||||
|
} else if (type === 'export') {
|
||||||
|
// Export bersifat sinkron, tidak bisa menunggu hasil geocode background
|
||||||
|
locationLabel = '-';
|
||||||
|
} else {
|
||||||
|
// Prioritas 3: kosong -> tampilkan placeholder dulu,
|
||||||
|
// nanti di-update otomatis oleh scheduleBackgroundGeocode()
|
||||||
|
// begitu hasil dari Nominatim datang.
|
||||||
|
locationLabel =
|
||||||
|
`<span id="${geoElementId(row, 'start')}" class="geo-pending">Load for location...</span>`;
|
||||||
}
|
}
|
||||||
return `
|
|
||||||
${moment.unix(data + AppState.TIMEFIX).format('DD MMM YYYY HH:mm:ss')}<br>
|
if (type === 'export') return locationLabel;
|
||||||
${Helper.shortenText(decodeURIComponent(row.startLoc || 'address'), 255) || "-"}
|
|
||||||
`;
|
return `${moment.unix(data + AppState.TIMEFIX).format('DD MMM YYYY HH:mm:ss')}<br>${locationLabel}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// data: 'finish',
|
|
||||||
// render: (data, type, row, meta) => {
|
|
||||||
// console.log(row)
|
|
||||||
// // saat export, hanya tampilkan lokasi tanpa tanggal/jam
|
|
||||||
// if (type === 'export') {
|
|
||||||
// return Helper.shortenText(decodeURIComponent(row.finishLoc ||
|
|
||||||
// 'address'), 255) || "-";
|
|
||||||
// }
|
|
||||||
// return `
|
|
||||||
// ${moment.unix(data + AppState.TIMEFIX).format('DD MMM YYYY HH:mm:ss')}<br>
|
|
||||||
// ${Helper.shortenText(decodeURIComponent(row.finishLoc || 'address'), 255) || "-"}
|
|
||||||
// `;
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
data: 'finish',
|
data: 'finish',
|
||||||
render: (data, type, row) => {
|
render: (data, type, row) => {
|
||||||
// console.log(row.fulladdress);
|
const {
|
||||||
// console.log(row.latitude);
|
lat: finishLat,
|
||||||
// console.log(row.longitude);
|
lng: finishLng
|
||||||
// console.log("End Loc : ",row.endLoc)
|
} = parseLatLong(row.finish_lat_long);
|
||||||
|
const nearestSite = findNearestSite(finishLat, finishLng);
|
||||||
// Saat export Excel
|
|
||||||
if (type === 'export') {
|
|
||||||
|
|
||||||
const nearestSite = findNearestSite(
|
|
||||||
row.finish_latitude,
|
|
||||||
row.finish_longitude
|
|
||||||
);
|
|
||||||
|
|
||||||
if (nearestSite) {
|
|
||||||
return formatSiteLabel(nearestSite);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Helper.shortenText(
|
|
||||||
decodeURIComponent(row.finishLoc || 'address'),
|
|
||||||
255
|
|
||||||
) || '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
const nearestSite = findNearestSite(
|
|
||||||
row.finish_latitude,
|
|
||||||
row.finish_longitude
|
|
||||||
);
|
|
||||||
|
|
||||||
|
let locationLabel;
|
||||||
if (nearestSite) {
|
if (nearestSite) {
|
||||||
return formatSiteLabel(nearestSite);
|
locationLabel = formatSiteLabel(nearestSite);
|
||||||
|
} else if (row.finishLoc) {
|
||||||
|
locationLabel = Helper.shortenText(decodeURIComponent(row
|
||||||
|
.finishLoc), 255);
|
||||||
|
} else if (type === 'export') {
|
||||||
|
locationLabel = '-';
|
||||||
|
} else {
|
||||||
|
locationLabel =
|
||||||
|
`<span id="${geoElementId(row, 'finish')}" class="geo-pending">Load for location...</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tampilan di tabel
|
if (type === 'export') return locationLabel;
|
||||||
return `
|
|
||||||
${moment.unix(data + AppState.TIMEFIX)
|
return `${moment.unix(data + AppState.TIMEFIX).format('DD MMM YYYY HH:mm:ss')}<br>${locationLabel}`;
|
||||||
.format('DD MMM YYYY HH:mm:ss')}
|
|
||||||
<br>
|
|
||||||
${
|
|
||||||
Helper.shortenText(
|
|
||||||
decodeURIComponent(row.finishLoc || 'address'),
|
|
||||||
255
|
|
||||||
) || '-'
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -698,7 +836,6 @@
|
|||||||
|
|
||||||
// Convert canvas to ImageState.
|
// Convert canvas to ImageState.
|
||||||
let imageData = ctx.getImageData(0, 0, imgWidth, imgHeight)
|
let imageData = ctx.getImageData(0, 0, imgWidth, imgHeight)
|
||||||
// console.log(imageState.data.byteLength + ' bytes.')
|
|
||||||
|
|
||||||
// Convert canvas to Blob
|
// Convert canvas to Blob
|
||||||
canvas.toBlob((blob) => {
|
canvas.toBlob((blob) => {
|
||||||
@ -706,13 +843,6 @@
|
|||||||
newReader.addEventListener('loadend', () => {
|
newReader.addEventListener('loadend', () => {
|
||||||
// Convert canvas to ArrayBuffer
|
// Convert canvas to ArrayBuffer
|
||||||
let arrayBuffer = newReader.result
|
let arrayBuffer = newReader.result
|
||||||
// console.log(arrayBuffer.byteLength + ' bytes.')
|
|
||||||
|
|
||||||
// Convert ArrayBuffer to Blob
|
|
||||||
// let blob = new Blob([arrayBuffer], {type: mimeType})
|
|
||||||
|
|
||||||
// Dispay Blob content in an Image.
|
|
||||||
// console.log(URL.createObjectURL(blob))
|
|
||||||
|
|
||||||
// Generate as file
|
// Generate as file
|
||||||
let newFile = new File([arrayBuffer], fileName, {
|
let newFile = new File([arrayBuffer], fileName, {
|
||||||
@ -730,6 +860,10 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Wrapper.activate();
|
// Pastikan data site sudah ter-load SEBELUM tabel trip dirender,
|
||||||
|
// supaya findNearestSite() punya data yang benar saat kolom start/finish di-render.
|
||||||
|
loadSiteLocations().always(function() {
|
||||||
|
Wrapper.activate();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@ -679,6 +679,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" id="mdlHistoryMaintenance"
|
||||||
|
aria-labelledby="mdlHistoryMaintenanceLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="mdlHistoryMaintenanceLabel">Maintenance History</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-sm align-middle" id="tHistoryMaintenance">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:35%">Service Type</th>
|
||||||
|
<th style="width:15%">Odometer</th>
|
||||||
|
<th style="width:15%">Date</th>
|
||||||
|
<th style="width:15%" class="text-center">Maintenance?</th>
|
||||||
|
<th style="width:20%" class="text-center">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tHistoryMaintenanceBody">
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center">No data</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('customjs')
|
@section('customjs')
|
||||||
@ -705,6 +740,7 @@
|
|||||||
VNew.activate();
|
VNew.activate();
|
||||||
VEdt.activate();
|
VEdt.activate();
|
||||||
VDel.activate();
|
VDel.activate();
|
||||||
|
VHistory.activate();
|
||||||
Filter.activate();
|
Filter.activate();
|
||||||
// images
|
// images
|
||||||
DUploadAdd.activate('fvhc');
|
DUploadAdd.activate('fvhc');
|
||||||
@ -805,14 +841,14 @@
|
|||||||
searchable: true,
|
searchable: true,
|
||||||
render: function (data, type, row, meta) {
|
render: function (data, type, row, meta) {
|
||||||
let action = `
|
let action = `
|
||||||
<a href="#" class="text-decoration-none me-1 btnEdtVhc" data-vid="${data}">
|
<a href="#" class="text-decoration-none me-1 btnEdtVhc" data-vid="${data}">
|
||||||
<span class="icon ion-eye fz-16"></span>
|
<span class="icon ion-eye fz-16"></span>
|
||||||
</a>
|
</a>
|
||||||
`;
|
<a href="#" class="text-decoration-none me-1 btnHistoryVhc" data-vid="${data}" data-catid="${row.cat_id}" title="Maintenance History">
|
||||||
// <a href="#" class="text-decoration-none text-danger btnDelVhc">
|
<span class="icon ion-clipboard fz-16"></span>
|
||||||
// <span class="icon ion-trash-b fz-16"></span>
|
</a>
|
||||||
// </a>
|
`;
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -2003,6 +2039,219 @@
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const VHistory = {
|
||||||
|
options_service: [],
|
||||||
|
activate: function () {
|
||||||
|
this.event();
|
||||||
|
},
|
||||||
|
event: function () {
|
||||||
|
// buka modal
|
||||||
|
$('#tVehicles').on('click', '.btnHistoryVhc', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const vid = $(this).data('vid');
|
||||||
|
const catid = $(this).data('catid');
|
||||||
|
$('#mdlHistoryMaintenance').data('vid', vid);
|
||||||
|
VHistory.load(vid, catid);
|
||||||
|
$('#mdlHistoryMaintenance').modal('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
// toggle Edit -> enable row
|
||||||
|
$('#tHistoryMaintenanceBody').on('click', '.btnEditRow', function () {
|
||||||
|
const $row = $(this).closest('tr');
|
||||||
|
VHistory.enableRow($row);
|
||||||
|
$(this)
|
||||||
|
.text('Save')
|
||||||
|
.removeClass('btn-outline-primary btnEditRow')
|
||||||
|
.addClass('btn-success btnSaveRow');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save row
|
||||||
|
$('#tHistoryMaintenanceBody').on('click', '.btnSaveRow', function () {
|
||||||
|
const $row = $(this).closest('tr');
|
||||||
|
VHistory.saveRow($row, $(this));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
load: async function (vid, catid) {
|
||||||
|
$('#tHistoryMaintenanceBody').html('<tr><td colspan="5" class="text-center">Loading...</td></tr>');
|
||||||
|
try {
|
||||||
|
const respTypes = await VHistory.loadServiceTypes(catid);
|
||||||
|
VHistory.options_service = respTypes || [];
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "{{ route('api_get_data_maintenance', '') }}/" + vid,
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'x-api-key': Helper.getCookie('_trtk') },
|
||||||
|
success: (res) => {
|
||||||
|
if (res.meta.type != 'success') {
|
||||||
|
Helper.toast('Warning', 'just now', res.meta.message);
|
||||||
|
$('#tHistoryMaintenanceBody').html('<tr><td colspan="5" class="text-center">Failed to load data</td></tr>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
VHistory.render(res.data);
|
||||||
|
},
|
||||||
|
error: (jqXHR) => {
|
||||||
|
$('#tHistoryMaintenanceBody').html('<tr><td colspan="5" class="text-center">Failed to load data</td></tr>');
|
||||||
|
Helper.toast('Error', 'just now', jqXHR.responseJSON?.meta?.message || 'Failed to load history');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
$('#tHistoryMaintenanceBody').html('<tr><td colspan="5" class="text-center">Failed to load data</td></tr>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadServiceTypes: function (catid) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
$.ajax({
|
||||||
|
url: "{{ route('api_list_vehicle_maintenance_type') }}",
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'x-api-key': Helper.getCookie('_trtk') },
|
||||||
|
success: (res) => resolve(res.data || []),
|
||||||
|
error: () => resolve([]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
render: function (rows) {
|
||||||
|
if (!rows || rows.length < 1) {
|
||||||
|
$('#tHistoryMaintenanceBody').html('<tr><td colspan="5" class="text-center">No data</td></tr>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
rows.forEach((row) => {
|
||||||
|
const selectedIds = (row.idservice || '').split(',').map((v) => v.trim());
|
||||||
|
const options = VHistory.options_service
|
||||||
|
.map((opt) => `<option value="${opt.id}" ${selectedIds.includes(String(opt.id)) ? 'selected' : ''}>${opt.service_type}</option>`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
const isChecked = Number(row.ismaintenance) === 1 ? 'checked' : '';
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<tr data-hid="${row.id}">
|
||||||
|
<td>
|
||||||
|
<select class="form-select form-select-sm history-service" multiple disabled style="width:100%;">
|
||||||
|
${options}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="number" class="form-control form-control-sm history-odometer" value="${row.odometer ?? ''}" disabled>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" class="form-control form-control-sm history-date" value="${row.dates ?? ''}" disabled autocomplete="off">
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<div class="form-check form-switch d-flex justify-content-center">
|
||||||
|
<input class="form-check-input history-ismaintenance" type="checkbox" role="switch" ${isChecked} disabled>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary btnEditRow">Edit</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#tHistoryMaintenanceBody').html(html);
|
||||||
|
|
||||||
|
// init select2 tiap row (tetap disabled dulu)
|
||||||
|
$('#tHistoryMaintenanceBody .history-service').each(function () {
|
||||||
|
$(this).select2({
|
||||||
|
dropdownParent: $('#mdlHistoryMaintenance'),
|
||||||
|
width: '100%',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// init datepicker tiap row
|
||||||
|
$('#tHistoryMaintenanceBody .history-date').datepicker({
|
||||||
|
format: 'yyyy-mm-dd',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
enableRow: function ($row) {
|
||||||
|
$row.find('.history-odometer').prop('disabled', false);
|
||||||
|
$row.find('.history-date').prop('disabled', false);
|
||||||
|
$row.find('.history-ismaintenance').prop('disabled', false);
|
||||||
|
|
||||||
|
const $select = $row.find('.history-service');
|
||||||
|
$select.prop('disabled', false);
|
||||||
|
$select.select2('destroy');
|
||||||
|
$select.select2({
|
||||||
|
dropdownParent: $('#mdlHistoryMaintenance'),
|
||||||
|
width: '100%',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
disableRow: function ($row) {
|
||||||
|
$row.find('.history-odometer').prop('disabled', true);
|
||||||
|
$row.find('.history-date').prop('disabled', true);
|
||||||
|
$row.find('.history-ismaintenance').prop('disabled', true);
|
||||||
|
|
||||||
|
const $select = $row.find('.history-service');
|
||||||
|
$select.prop('disabled', true);
|
||||||
|
$select.select2('destroy');
|
||||||
|
$select.select2({
|
||||||
|
dropdownParent: $('#mdlHistoryMaintenance'),
|
||||||
|
width: '100%',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
saveRow: function ($row, $btn) {
|
||||||
|
const vid = $('#mdlHistoryMaintenance').data('vid');
|
||||||
|
const hid = $row.data('hid');
|
||||||
|
const idservice = $row.find('.history-service').val(); // array
|
||||||
|
const odometer = $row.find('.history-odometer').val();
|
||||||
|
const dates = $row.find('.history-date').val();
|
||||||
|
const ismaintenance = $row.find('.history-ismaintenance').is(':checked') ? 1 : 0;
|
||||||
|
|
||||||
|
if (!idservice || idservice.length < 1) {
|
||||||
|
Helper.toast('Validasi', 'just now', 'Service type wajib diisi');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!odometer) {
|
||||||
|
Helper.toast('Validasi', 'just now', 'Odometer wajib diisi');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!dates) {
|
||||||
|
Helper.toast('Validasi', 'just now', 'Tanggal wajib diisi');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$btn.prop('disabled', true).text('Saving...');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "{{ route('api_update_data_maintenance', ['vehicleid' => '__VID__', 'id' => '__ID__']) }}"
|
||||||
|
.replace('__VID__', vid)
|
||||||
|
.replace('__ID__', hid),
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'x-api-key': Helper.getCookie('_trtk'),
|
||||||
|
'x-csrf-token': $('meta[name="csrf-token"]').attr('content'),
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
idservice: idservice.join(','),
|
||||||
|
odometer: odometer,
|
||||||
|
dates: dates,
|
||||||
|
ismaintenance: ismaintenance,
|
||||||
|
},
|
||||||
|
success: (res) => {
|
||||||
|
$btn.prop('disabled', false);
|
||||||
|
if (res.meta.type != 'success') {
|
||||||
|
Helper.toast('Warning', 'just now', res.meta.message);
|
||||||
|
$btn.text('Save');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Helper.toast('Success', 'just now', 'Berhasil update maintenance history');
|
||||||
|
VHistory.disableRow($row);
|
||||||
|
$btn.text('Edit')
|
||||||
|
.removeClass('btn-success btnSaveRow')
|
||||||
|
.addClass('btn-outline-primary btnEditRow');
|
||||||
|
},
|
||||||
|
error: (jqXHR) => {
|
||||||
|
$btn.prop('disabled', false).text('Save');
|
||||||
|
if (jqXHR.status >= 500) {
|
||||||
|
Helper.toast('Error', 'just now', 'Please try again');
|
||||||
|
} else {
|
||||||
|
Helper.toast('Error', 'just now', jqXHR.responseJSON?.meta?.message || 'Gagal menyimpan');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
Wrapper.activate();
|
Wrapper.activate();
|
||||||
</script>
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
@ -87,6 +87,13 @@
|
|||||||
<a class="dropdown-item {{ Request::segment(2) == 'truck_types' ? 'active' : '' }}" href="{{ route('view_config_truck_types') }}" title="">Vehicle Type</a>
|
<a class="dropdown-item {{ Request::segment(2) == 'truck_types' ? 'active' : '' }}" href="{{ route('view_config_truck_types') }}" title="">Vehicle Type</a>
|
||||||
</li>
|
</li>
|
||||||
@endcan
|
@endcan
|
||||||
|
@can('trips_label.view')
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item {{ Request::segment(2) == 'trips_label' ? 'active' : '' }}" aria-current="page" href="{{ route('view_trips_label') }}">
|
||||||
|
Trips Label
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
@endcan
|
||||||
@can('vehicle_maintenance_type.view')
|
@can('vehicle_maintenance_type.view')
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item {{ Request::segment(2) == 'maintenance_type' ? 'active' : '' }}" href="{{ route('view_vehicle_maintenance_type') }}" title="">Maintenance Type</a>
|
<a class="dropdown-item {{ Request::segment(2) == 'maintenance_type' ? 'active' : '' }}" href="{{ route('view_vehicle_maintenance_type') }}" title="">Maintenance Type</a>
|
||||||
@ -155,6 +162,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@endcan
|
@endcan
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="navbar-nav ms-auto my-2 my-lg-0 navbar-nav-scroll">
|
<ul class="navbar-nav ms-auto my-2 my-lg-0 navbar-nav-scroll">
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
|
|||||||
@ -145,6 +145,10 @@ Route::middleware(["auth", "auth.user"])->group(function () {
|
|||||||
->name("view_vehicle_maintenance_type")
|
->name("view_vehicle_maintenance_type")
|
||||||
->middleware("permission:vehicle_maintenance_type.view");
|
->middleware("permission:vehicle_maintenance_type.view");
|
||||||
|
|
||||||
|
Route::get("/trips_label", "TripsLabelingController@view_trips_label")
|
||||||
|
->name("view_trips_label")
|
||||||
|
->middleware("permission:trips_label.view");
|
||||||
|
|
||||||
Route::get("/config/devices", "DevicesController@view_devices")
|
Route::get("/config/devices", "DevicesController@view_devices")
|
||||||
->name("view_config_devices")
|
->name("view_config_devices")
|
||||||
->middleware("permission:config_master_device.view");
|
->middleware("permission:config_master_device.view");
|
||||||
@ -334,6 +338,9 @@ Route::middleware(["auth", "auth.user"])->group(function () {
|
|||||||
"api_search_nopol"
|
"api_search_nopol"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Route::get("/api/maintenances/{vid}", "MaintenanceHistoryController@api_get_data_maintenance")->name("api_get_data_maintenance");
|
||||||
|
Route::put("/api/maintenances/{vehicleid}/{id}", "MaintenanceHistoryController@api_update_data_maintenance")->name("api_update_data_maintenance");
|
||||||
|
|
||||||
Route::get("/api/zones", "ZoneController@api_list_zones")->name("api_list_zones");
|
Route::get("/api/zones", "ZoneController@api_list_zones")->name("api_list_zones");
|
||||||
Route::get("/api/zones/{zid}", "ZoneController@api_show_zone")->name("api_show_zone");
|
Route::get("/api/zones/{zid}", "ZoneController@api_show_zone")->name("api_show_zone");
|
||||||
Route::post("/api/zones", "ZoneController@api_add_zone")->name("api_add_zone");
|
Route::post("/api/zones", "ZoneController@api_add_zone")->name("api_add_zone");
|
||||||
@ -633,6 +640,24 @@ Route::middleware(["auth", "auth.user"])->group(function () {
|
|||||||
"api_del_vehicle_maintenance_type"
|
"api_del_vehicle_maintenance_type"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Route::get("/api/trips_label/{id}", "TripsLabelingController@api_show_trips_label")->name(
|
||||||
|
"api_show_trips_label"
|
||||||
|
);
|
||||||
|
|
||||||
|
Route::get("/api/trips_label", "TripsLabelingController@api_list_trips_label")->name(
|
||||||
|
"api_list_trips_label"
|
||||||
|
);
|
||||||
|
|
||||||
|
Route::post("/api/trips_label", "TripsLabelingController@api_add_trips_label")->name(
|
||||||
|
"api_add_trips_label"
|
||||||
|
);
|
||||||
|
Route::put("/api/trips_label/{id}", "TripsLabelingController@api_edit_trips_label")->name(
|
||||||
|
"api_edit_trips_label"
|
||||||
|
);
|
||||||
|
Route::delete("/api/trips_label/{id}", "TripsLabelingController@api_del_trips_label")->name(
|
||||||
|
"api_del_trips_label"
|
||||||
|
);
|
||||||
|
|
||||||
Route::get("/api/devices", "DevicesController@api_list_devices")->name("api_list_devices");
|
Route::get("/api/devices", "DevicesController@api_list_devices")->name("api_list_devices");
|
||||||
Route::get("/api/devices/{id}", "DevicesController@api_show_device")->name("api_show_device");
|
Route::get("/api/devices/{id}", "DevicesController@api_show_device")->name("api_show_device");
|
||||||
Route::post("/api/devices", "DevicesController@api_add_device")->name("api_add_device");
|
Route::post("/api/devices", "DevicesController@api_add_device")->name("api_add_device");
|
||||||
|
|||||||
Reference in New Issue
Block a user