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');
|
||||
$from_date = $req->input('from_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');
|
||||
$poolcode = $req->input('poolcode');
|
||||
// $from_date = 1756054800;
|
||||
// $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
|
||||
$date = Carbon::createFromTimestamp($from_date);
|
||||
$yymm = $date->format('ym');
|
||||
@ -166,57 +178,70 @@ class ReportsController extends Controller
|
||||
// and if(? , vt.id = ? , 1=1)
|
||||
// and if(? , t.pool_code = ? , 1=1)
|
||||
// ", [$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 =
|
||||
"WITH TotalMileage AS (
|
||||
SELECT
|
||||
id,
|
||||
SUM(mileage) AS total_mileage,
|
||||
COUNT(*) AS total_trip
|
||||
FROM trips
|
||||
WHERE start BETWEEN ? AND ?
|
||||
GROUP BY id
|
||||
)
|
||||
SELECT
|
||||
t.*,
|
||||
ttll.start_latitude,
|
||||
ttll.start_longitude,
|
||||
ttll.finish_latitude,
|
||||
ttll.finish_longitude,
|
||||
tm.total_mileage,
|
||||
tm.total_trip,
|
||||
vt.name AS type_name,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY t.id
|
||||
ORDER BY t.start
|
||||
) AS trip_id
|
||||
FROM trips t
|
||||
JOIN TotalMileage tm ON t.id = tm.id
|
||||
LEFT JOIN t_vehicles tvt ON tvt.id = t.vhc_id
|
||||
LEFT JOIN t_vehicles_cats vt ON vt.id = tvt.cat_id
|
||||
LEFT JOIN t_trips_latitude_longitude ttll ON t.id = ttll.tripsid
|
||||
WHERE
|
||||
t.start BETWEEN ? AND ?
|
||||
AND (? IS NULL OR t.id = ?)
|
||||
AND (? IS NULL OR vt.id = ?)
|
||||
AND (? IS NULL OR t.pool_code = ?);
|
||||
";
|
||||
id,
|
||||
SUM(mileage) AS total_mileage,
|
||||
COUNT(*) AS total_trip
|
||||
FROM trips
|
||||
WHERE start BETWEEN ? AND ?
|
||||
GROUP BY id
|
||||
)
|
||||
SELECT
|
||||
t.*,
|
||||
tm.total_mileage,
|
||||
tm.total_trip,
|
||||
vt.name AS type_name,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY t.id
|
||||
ORDER BY t.start
|
||||
) AS trip_id
|
||||
FROM trips t
|
||||
JOIN TotalMileage tm ON t.id = tm.id
|
||||
LEFT JOIN t_vehicles tvt ON tvt.id = t.vhc_id
|
||||
LEFT JOIN t_vehicles_cats vt ON vt.id = tvt.cat_id
|
||||
WHERE
|
||||
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;
|
||||
$poolcode = $poolcode ?: null;
|
||||
|
||||
$list = DB::select($sql, [
|
||||
$bindings = [
|
||||
$from_date,
|
||||
$to_date,
|
||||
$from_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,
|
||||
$poolcode,
|
||||
$poolcode,
|
||||
]);
|
||||
|
||||
$list = DB::select($sql, $bindings);
|
||||
|
||||
// // RETURN 1 - LIST
|
||||
// if($req->type != '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();
|
||||
|
||||
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"]);
|
||||
}
|
||||
|
||||
|
||||
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')
|
||||
<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')
|
||||
@ -25,8 +31,8 @@
|
||||
@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> -->
|
||||
<button id="btnMdlNewVhc" class="btn btn-sm btn-danger">Add New Vehicle</button>
|
||||
</div> -->
|
||||
@endcan
|
||||
|
||||
{{-- <div class="col-auto text-end ps-0">
|
||||
@ -59,7 +65,8 @@
|
||||
<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">
|
||||
<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>
|
||||
@ -128,74 +135,6 @@
|
||||
<th class="">Detail</th>
|
||||
</tr>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@ -206,18 +145,6 @@
|
||||
<!-- modal -->
|
||||
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" id="mdlDetailTrip"
|
||||
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>
|
||||
@endsection
|
||||
|
||||
@ -227,9 +154,6 @@
|
||||
<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://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"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
<script src="https://unpkg.com/leaflet-routing-machine@3.2.12/dist/leaflet-routing-machine.js"></script>
|
||||
@ -253,15 +177,12 @@
|
||||
activate: function() {
|
||||
Wrapper.init();
|
||||
Wrapper.event();
|
||||
// DTable.table = $('#tVehicleTrips').DataTable()
|
||||
// DTable.activate();
|
||||
},
|
||||
init: () => {
|
||||
$('#tgl0, #tgl1').datetimepicker({
|
||||
format: 'd-m-Y H:i',
|
||||
defaultTime: '00:00',
|
||||
closeOnDateSelect: true,
|
||||
// mask:true
|
||||
});
|
||||
|
||||
},
|
||||
@ -282,44 +203,71 @@
|
||||
const tgl1 = $(this).data('tgl1');
|
||||
const nopol1 = $(this).data('nopol1');
|
||||
|
||||
// $("#mdlDetailTrip").load("{{ route('view_report_trip_detail') }}");
|
||||
$('#mdlDetailTrip').empty().load("{{ route('view_report_trip_detail') }}",
|
||||
`vid=${vid}&tgl0=${tgl0}&tgl1=${tgl1}&nopol1=${nopol1}`, () => {
|
||||
$('#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
|
||||
function getDistanceMeters(lat1, lon1, lat2, lon2) {
|
||||
@ -335,6 +283,7 @@
|
||||
}
|
||||
|
||||
// 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;
|
||||
@ -343,7 +292,7 @@
|
||||
let minDist = Infinity;
|
||||
SITE_LOCATIONS.forEach(site => {
|
||||
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;
|
||||
nearest = site;
|
||||
}
|
||||
@ -352,10 +301,212 @@
|
||||
}
|
||||
|
||||
// Format label sesuai output yang diinginkan
|
||||
// address dari API sudah lengkap (mis. "Bitterlaun, Hera, Cristo Rei, Dili")
|
||||
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 = {
|
||||
table: null,
|
||||
lastAjax: null, // keep track of the last ajax request
|
||||
@ -399,7 +550,28 @@
|
||||
`,
|
||||
type: 'GET',
|
||||
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);
|
||||
|
||||
// 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') {
|
||||
@ -469,96 +641,62 @@
|
||||
// ==== END KOLOM BARU ====
|
||||
{
|
||||
data: 'start',
|
||||
render: (data, type, row, meta) => {
|
||||
// saat export, hanya tampilkan lokasi tanpa tanggal/jam
|
||||
if (type === 'export') {
|
||||
const nearestSite = findNearestSite(
|
||||
row.start_latitude,
|
||||
row.start_longitude
|
||||
);
|
||||
|
||||
if (nearestSite) {
|
||||
return formatSiteLabel(nearestSite);
|
||||
}
|
||||
return Helper.shortenText(decodeURIComponent(row.startLoc ||
|
||||
'address'), 255) || "-";
|
||||
}
|
||||
const nearestSite = findNearestSite(
|
||||
row.start_latitude,
|
||||
row.start_longitude
|
||||
);
|
||||
render: (data, type, row) => {
|
||||
const {
|
||||
lat: startLat,
|
||||
lng: startLng
|
||||
} = parseLatLong(row.start_lat_long);
|
||||
const nearestSite = findNearestSite(startLat, startLng);
|
||||
|
||||
let locationLabel;
|
||||
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>
|
||||
${Helper.shortenText(decodeURIComponent(row.startLoc || 'address'), 255) || "-"}
|
||||
`;
|
||||
|
||||
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, 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',
|
||||
render: (data, type, row) => {
|
||||
// console.log(row.fulladdress);
|
||||
// console.log(row.latitude);
|
||||
// console.log(row.longitude);
|
||||
// console.log("End Loc : ",row.endLoc)
|
||||
|
||||
// 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
|
||||
);
|
||||
const {
|
||||
lat: finishLat,
|
||||
lng: finishLng
|
||||
} = parseLatLong(row.finish_lat_long);
|
||||
const nearestSite = findNearestSite(finishLat, finishLng);
|
||||
|
||||
let locationLabel;
|
||||
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
|
||||
return `
|
||||
${moment.unix(data + AppState.TIMEFIX)
|
||||
.format('DD MMM YYYY HH:mm:ss')}
|
||||
<br>
|
||||
${
|
||||
Helper.shortenText(
|
||||
decodeURIComponent(row.finishLoc || 'address'),
|
||||
255
|
||||
) || '-'
|
||||
}
|
||||
`;
|
||||
if (type === 'export') return locationLabel;
|
||||
|
||||
return `${moment.unix(data + AppState.TIMEFIX).format('DD MMM YYYY HH:mm:ss')}<br>${locationLabel}`;
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -698,7 +836,6 @@
|
||||
|
||||
// Convert canvas to ImageState.
|
||||
let imageData = ctx.getImageData(0, 0, imgWidth, imgHeight)
|
||||
// console.log(imageState.data.byteLength + ' bytes.')
|
||||
|
||||
// Convert canvas to Blob
|
||||
canvas.toBlob((blob) => {
|
||||
@ -706,13 +843,6 @@
|
||||
newReader.addEventListener('loadend', () => {
|
||||
// Convert canvas to ArrayBuffer
|
||||
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
|
||||
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>
|
||||
@endsection
|
||||
|
||||
@ -679,6 +679,41 @@
|
||||
</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
|
||||
|
||||
@section('customjs')
|
||||
@ -705,6 +740,7 @@
|
||||
VNew.activate();
|
||||
VEdt.activate();
|
||||
VDel.activate();
|
||||
VHistory.activate();
|
||||
Filter.activate();
|
||||
// images
|
||||
DUploadAdd.activate('fvhc');
|
||||
@ -805,14 +841,14 @@
|
||||
searchable: true,
|
||||
render: function (data, type, row, meta) {
|
||||
let action = `
|
||||
<a href="#" class="text-decoration-none me-1 btnEdtVhc" data-vid="${data}">
|
||||
<span class="icon ion-eye fz-16"></span>
|
||||
</a>
|
||||
`;
|
||||
// <a href="#" class="text-decoration-none text-danger btnDelVhc">
|
||||
// <span class="icon ion-trash-b fz-16"></span>
|
||||
// </a>
|
||||
return action;
|
||||
<a href="#" class="text-decoration-none me-1 btnEdtVhc" data-vid="${data}">
|
||||
<span class="icon ion-eye fz-16"></span>
|
||||
</a>
|
||||
<a href="#" class="text-decoration-none me-1 btnHistoryVhc" data-vid="${data}" data-catid="${row.cat_id}" title="Maintenance History">
|
||||
<span class="icon ion-clipboard fz-16"></span>
|
||||
</a>
|
||||
`;
|
||||
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();
|
||||
</script>
|
||||
@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>
|
||||
</li>
|
||||
@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')
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
@endcan
|
||||
|
||||
</ul>
|
||||
<ul class="navbar-nav ms-auto my-2 my-lg-0 navbar-nav-scroll">
|
||||
<li class="nav-item dropdown">
|
||||
|
||||
@ -145,6 +145,10 @@ Route::middleware(["auth", "auth.user"])->group(function () {
|
||||
->name("view_vehicle_maintenance_type")
|
||||
->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")
|
||||
->name("view_config_devices")
|
||||
->middleware("permission:config_master_device.view");
|
||||
@ -334,6 +338,9 @@ Route::middleware(["auth", "auth.user"])->group(function () {
|
||||
"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/{zid}", "ZoneController@api_show_zone")->name("api_show_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"
|
||||
);
|
||||
|
||||
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/{id}", "DevicesController@api_show_device")->name("api_show_device");
|
||||
Route::post("/api/devices", "DevicesController@api_add_device")->name("api_add_device");
|
||||
|
||||
Reference in New Issue
Block a user