Compare commits

..

12 Commits

Author SHA1 Message Date
cd60b836b5 update 2026-08-05 09:28:06 +07:00
85279d4616 UPDATE: LOCATION AND KM JUMPING DONE 2026-08-05 08:42:33 +07:00
ff47f3f932 update 2026-08-01 21:22:55 +07:00
994608327a update: if address start and finish null, change to address from latitude and longitude 2026-08-01 20:39:20 +07:00
cbca01a8e2 update 2026-07-20 11:28:06 +07:00
0b591da182 update 2026-07-20 10:53:53 +07:00
dfa517f3a1 update trips label done 2026-07-20 10:53:30 +07:00
bb18878336 update 2026-07-20 09:49:29 +07:00
32229a1b52 update 2026-07-14 10:55:42 +07:00
583ea3b9a0 update vehicles history maintenance 2026-07-13 11:52:15 +07:00
a0ce09cdda update latitude and longitude for trips 2026-07-13 10:48:38 +07:00
a19e12ec7b update vehicle maintenance type done 2026-07-13 09:51:20 +07:00
12 changed files with 3294 additions and 231 deletions

View 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"]);
}
}
}

View File

@ -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,6 +178,16 @@ 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
@ -191,27 +213,35 @@ class ReportsController extends Controller
LEFT JOIN t_vehicles_cats vt ON vt.id = tvt.cat_id
WHERE
t.start BETWEEN ? AND ?
AND (? IS NULL OR t.id = ?)
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");

View 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"]);
}
}
}

View File

@ -0,0 +1,225 @@
<?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 VehicleMaintenanceController extends Controller
{
public function api_show_maintenance_type(Request $req, $id)
{
try {
$data = DB::table('t_maintenance_vhc_type')
->where('id', $id)
->first();
if (!$data) {
$apiResp = Responses::error("Vehicle Maintenance type 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_vehicle_maintenance_type(Request $req)
{
$data = [];
$log = [
"module" => "Vehicle Maintenance Type",
"action" => "View",
"desc" => "Open Vehicle Maintenance Type",
];
UserLogs::insert(Auth::user()->id, $log);
return view("menu_v1.configs.vehicle_maintenance", $data);
}
/**
* API
*/
public function api_list_vehicle_maintenance_type(Request $req)
{
try {
$datas = DB::table('t_maintenance_vhc_type')
->where('isdeleted', 0)
->get();
foreach ($datas as $key => $row) {
$datas[$key]->DT_RowIndex = $key + 1;
$datas[$key]->action = "-";
}
$apiResp = Responses::success("success list vehicle maintenance");
$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_vehicle_maintenance_type(Request $req)
{
try {
$now = time();
$input = [
"service_type" => $req->service_type,
];
$rulesInput = [
"service_type" => "required|string",
];
// 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 = [
"service_type" => $req->service_type,
"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_maintenance_vhc_type')->insert($insert);
$apiResp = Responses::created("success add new vehicle maintenance type");
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"]);
}
}
/**
* Update the specified resource in storage.
*
* @param Request $req
* @param int $id
* @return Response
*/
public function api_edit_vehicle_maintenance_type(Request $req, $id)
{
try {
$now = time();
$input = [
"service_type" => $req->service_type,
];
$rulesInput = [
"service_type" => "required|string",
];
// 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 = [
"service_type" => $req->service_type,
"udt" => date("Y-m-d H:i:s", $now),
"uby" => Auth::user()->id
];
DB::table('t_maintenance_vhc_type')->where('id', $id)->update($update);
$apiResp = Responses::created("success update vehicle meintenance type");
DB::commit();
$log = [
"module" => "Vehicle Type",
"action" => "Update",
"desc" => "Update vehicle maintenance type: " . $req->type_name,
];
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_vehicle_maintenance_type($id)
{
try {
DB::beginTransaction();
DB::table('t_maintenance_vhc_type')->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 vehicle maintenance type");
$log = [
"module" => "Vehicle Type",
"action" => "Delete",
"desc" => "Delete vehicle maintenance type: " . $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"]);
}
}
}

View 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

View File

@ -0,0 +1,576 @@
@extends('app.app')
@section('title')
Conf Vehicle Types
@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">Vehicle Maintenance Types</p>
</div>
@can('config_truck_type.create')
<div class="col text-end">
<button id="btnMdlNewTruckType" class="btn btn-sm btn-danger">Add New Vehicle Maintenance
Type</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">Service Type Name</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 Maintenance Vehicle Type</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
{{-- Maintenance Type Name Type --}}
<div class="mb-3">
<div class="border-bottom">
<h6>Maintenance Vehicle Type</h6>
</div>
<div class="row">
<div class="col-sm-6 col-md-6">
<label for="service_type" class="col-form-label">Maintenance Type Name:</label>
<input type="text" name="service_type" id="service_type" class="form-control"
placeholder="Please consider use english language">
</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 Vehicle Maintenance Type</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>Maintenance Type</h6>
</div>
<div class="row">
<div class="col-sm-6 col-md-6">
<label for="service_type_edit" class="col-form-label">Maintenance Type Name:</label>
<input type="text" name="service_type" id="service_type_edit" class="form-control"
placeholder="Please consider use english language">
</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 Maintenance Vehicle</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 maintenance vehicle type
<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_vehicle_maintenance_type') }}",
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: 'service_type',
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.service_type = $('#service_type').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_vehicle_maintenance_type') }}",
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 vehicle maintenance type');
$('#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('Vehicle Type 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_maintenance_type', '') }}/" + 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) {
$('#service_type_edit').val(data.service_type);
$('#mdlEdtTruckType').data('id', data.id);
$('#mdlEdtTruckType').modal('show');
},
getData: function() {
let data = {};
data.ttid = $('#mdlEdtTruckType').data('id');
data.service_type = $('#service_type_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_vehicle_maintenance_type', '') }}/" + 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 maintenance vehicle type');
$('#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,
type_name: data.type_name,
});
$('#mdlDelTruckType').data('id', data.ttid);
$('#mdlDelTruckType').modal('show');
});
},
passDataToView: function(data) {
$('#del-type_name').text(data.service_type);
},
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_vehicle_maintenance_type', '') }}/" + 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 vehicle type');
$('#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

View 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

View File

@ -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')
@ -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,71 +641,62 @@
// ==== END KOLOM BARU ====
{
data: 'start',
render: (data, type, row, meta) => {
// saat export, hanya tampilkan lokasi tanpa tanggal/jam
if (type === 'export') {
return Helper.shortenText(decodeURIComponent(row.startLoc ||
'address'), 255) || "-";
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>`;
}
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)
const {
lat: finishLat,
lng: finishLng
} = parseLatLong(row.finish_lat_long);
const nearestSite = findNearestSite(finishLat, finishLng);
// Saat export Excel
if (type === 'export') {
// const nearestSite = findNearestSite(
// row.latitude,
// row.longitude
// );
// if (nearestSite) {
// return formatSiteLabel(nearestSite);
// }
return Helper.shortenText(
decodeURIComponent(row.finishLoc || 'address'),
255
) || '-';
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>`;
}
// 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}`;
}
},
{
@ -673,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) => {
@ -681,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, {
@ -705,6 +860,10 @@
});
}
// 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

View File

@ -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');
@ -808,10 +844,10 @@
<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>
`;
// <a href="#" class="text-decoration-none text-danger btnDelVhc">
// <span class="icon ion-trash-b 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

View File

@ -75,6 +75,7 @@
auth()->user()->can('config_distribution_category.view') ||
auth()->user()->can('config_pool.view') ||
auth()->user()->can('user_logs.view')
|| auth()->user()->can('config_vehicle_maintenance_type.view')
)
<li class="nav-item dropdown {{ Request::segment(1) == 'config' ? 'active' : '' }}">
<a class="nav-link dropdown-toggle" href="#" id="dropdownConfig" role="button" data-bs-toggle="dropdown" aria-expanded="false">
@ -86,6 +87,18 @@
<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>
</li>
@endcan
@can('config_master_device.view')
<li>
<a class="dropdown-item {{ Request::segment(2) == 'devices' ? 'active' : '' }}" href="{{ route('view_config_devices') }}" title="">Master Devices</a>
@ -149,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">

View File

@ -141,6 +141,14 @@ Route::middleware(["auth", "auth.user"])->group(function () {
->name("view_config_truck_types")
->middleware("permission:config_truck_type.view");
Route::get("/config/maintenance_type", "VehicleMaintenanceController@view_vehicle_maintenance_type")
->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");
@ -330,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");
@ -611,6 +622,42 @@ Route::middleware(["auth", "auth.user"])->group(function () {
"api_conf_del_truck_type"
);
Route::get("/api/conf/maintenance_type/{id}", "VehicleMaintenanceController@api_show_maintenance_type")->name(
"api_show_maintenance_type"
);
Route::get("/api/conf/maintenance_type", "VehicleMaintenanceController@api_list_vehicle_maintenance_type")->name(
"api_list_vehicle_maintenance_type"
);
Route::post("/api/conf/maintenance_type", "VehicleMaintenanceController@api_add_vehicle_maintenance_type")->name(
"api_add_vehicle_maintenance_type"
);
Route::put("/api/conf/maintenance_type/{id}", "VehicleMaintenanceController@api_edit_vehicle_maintenance_type")->name(
"api_edit_vehicle_maintenance_type"
);
Route::delete("/api/conf/maintenance_type/{id}", "VehicleMaintenanceController@api_del_vehicle_maintenance_type")->name(
"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");