fix filtering dealer and data with base on user login, partial update precheck and postcheck schema and view
This commit is contained in:
@@ -287,7 +287,11 @@ class ApiController extends Controller
|
||||
|
||||
public function logout()
|
||||
{
|
||||
Auth::user()->tokens()->delete();
|
||||
/** @var \App\Models\User $user */
|
||||
$user = auth('sanctum')->user();
|
||||
if ($user) {
|
||||
$user->tokens()->delete();
|
||||
}
|
||||
return response()->json([
|
||||
'message' => 'Logout success',
|
||||
'status' => true,
|
||||
|
||||
@@ -1060,7 +1060,6 @@ class TransactionController extends Controller
|
||||
'users.name as mechanic_name'
|
||||
])
|
||||
->where('transactions.dealer_id', $request->dealer_id)
|
||||
->where('users.role_id', 4) // Only transactions created by SA
|
||||
->whereIn('transactions.status', [0, 1]) // Only pending and completed transactions
|
||||
->orderBy('transactions.date', 'desc');
|
||||
|
||||
@@ -1091,6 +1090,7 @@ class TransactionController extends Controller
|
||||
$data = [];
|
||||
foreach ($transactions as $transaction) {
|
||||
$data[] = [
|
||||
'id' => $transaction->id,
|
||||
'date' => date('d/m/Y', strtotime($transaction->date)),
|
||||
'spk' => $transaction->spk,
|
||||
'police_number' => $transaction->police_number,
|
||||
@@ -1127,13 +1127,7 @@ class TransactionController extends Controller
|
||||
case 0: // pending
|
||||
return '<span class="badge badge-warning">Menunggu</span>';
|
||||
case 1: // completed
|
||||
return '<span class="badge badge-success">Selesai</span>';
|
||||
case 2: // in_progress
|
||||
return '<span class="badge badge-primary">Sedang Dikerjakan</span>';
|
||||
case 3: // claimed
|
||||
return '<span class="badge badge-info">Diklaim</span>';
|
||||
case 4: // cancelled
|
||||
return '<span class="badge badge-danger">Dibatalkan</span>';
|
||||
return '<span class="badge badge-success">Closed</span>';
|
||||
default:
|
||||
return '<span class="badge badge-secondary">Tidak Diketahui</span>';
|
||||
}
|
||||
@@ -1234,6 +1228,25 @@ class TransactionController extends Controller
|
||||
$buttons .= 'Klaim';
|
||||
$buttons .= '</button>';
|
||||
} else {
|
||||
if($transaction->claimed_by == Auth::user()->id) {
|
||||
// Check if precheck exists
|
||||
$precheck = \App\Models\Precheck::where('transaction_id', $transaction->id)->first();
|
||||
if (!$precheck) {
|
||||
$buttons .= '<a href="/transaction/prechecks/' . $transaction->id . '" class="btn btn-sm btn-warning mr-1" title="Precheck">';
|
||||
$buttons .= 'Precheck';
|
||||
$buttons .= '</a>';
|
||||
} else {
|
||||
// Check if postcheck exists
|
||||
$postcheck = \App\Models\Postcheck::where('transaction_id', $transaction->id)->first();
|
||||
if (!$postcheck) {
|
||||
$buttons .= '<a href="/transaction/postchecks/' . $transaction->id . '" class="btn btn-sm btn-info mr-1" title="Postcheck">';
|
||||
$buttons .= 'Postcheck';
|
||||
$buttons .= '</a>';
|
||||
} else {
|
||||
$buttons .= '<span class="badge badge-success">Selesai</span>';
|
||||
}
|
||||
}
|
||||
}
|
||||
$buttons .= '<span class="badge badge-info">Sudah Diklaim</span>';
|
||||
}
|
||||
}
|
||||
|
||||
72
app/Http/Controllers/Transactions/PostchecksController.php
Normal file
72
app/Http/Controllers/Transactions/PostchecksController.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Transactions;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Postcheck;
|
||||
use App\Models\Transaction;
|
||||
|
||||
class PostchecksController extends Controller
|
||||
{
|
||||
public function index(Transaction $transaction)
|
||||
{
|
||||
$acConditions = Postcheck::getAcConditionOptions();
|
||||
$blowerConditions = Postcheck::getBlowerConditionOptions();
|
||||
$evaporatorConditions = Postcheck::getEvaporatorConditionOptions();
|
||||
$compressorConditions = Postcheck::getCompressorConditionOptions();
|
||||
|
||||
return view('transaction.postchecks', compact(
|
||||
'transaction',
|
||||
'acConditions',
|
||||
'blowerConditions',
|
||||
'evaporatorConditions',
|
||||
'compressorConditions'
|
||||
));
|
||||
}
|
||||
|
||||
public function store(Request $request, Transaction $transaction)
|
||||
{
|
||||
$request->validate([
|
||||
'kilometer' => 'required|numeric|min:0',
|
||||
'pressure_high' => 'required|numeric|min:0',
|
||||
'pressure_low' => 'nullable|numeric|min:0',
|
||||
'cabin_temperature' => 'nullable|numeric',
|
||||
'cabin_temperature_image' => 'nullable|string',
|
||||
'ac_condition' => 'nullable|in:' . implode(',', Postcheck::getAcConditionOptions()),
|
||||
'ac_image' => 'nullable|string',
|
||||
'blower_condition' => 'nullable|in:' . implode(',', Postcheck::getBlowerConditionOptions()),
|
||||
'blower_image' => 'nullable|string',
|
||||
'evaporator_condition' => 'nullable|in:' . implode(',', Postcheck::getEvaporatorConditionOptions()),
|
||||
'evaporator_image' => 'nullable|string',
|
||||
'compressor_condition' => 'nullable|in:' . implode(',', Postcheck::getCompressorConditionOptions()),
|
||||
'postcheck_notes' => 'nullable|string',
|
||||
'front_image' => 'required|string',
|
||||
]);
|
||||
|
||||
// Pastikan transaction_id sama dengan $transaction->id
|
||||
$postcheck = Postcheck::create([
|
||||
'transaction_id' => $transaction->id,
|
||||
'postcheck_by' => auth()->id(),
|
||||
'postcheck_at' => now(),
|
||||
'police_number' => $transaction->police_number,
|
||||
'spk_number' => $transaction->spk,
|
||||
'front_image' => $request->front_image,
|
||||
'kilometer' => $request->kilometer,
|
||||
'pressure_high' => $request->pressure_high,
|
||||
'pressure_low' => $request->pressure_low,
|
||||
'cabin_temperature' => $request->cabin_temperature,
|
||||
'cabin_temperature_image' => $request->cabin_temperature_image,
|
||||
'ac_condition' => $request->ac_condition,
|
||||
'ac_image' => $request->ac_image,
|
||||
'blower_condition' => $request->blower_condition,
|
||||
'blower_image' => $request->blower_image,
|
||||
'evaporator_condition' => $request->evaporator_condition,
|
||||
'evaporator_image' => $request->evaporator_image,
|
||||
'compressor_condition' => $request->compressor_condition,
|
||||
'postcheck_notes' => $request->postcheck_notes,
|
||||
]);
|
||||
|
||||
return redirect()->route('transaction')->with('success', 'Postcheck berhasil disimpan');
|
||||
}
|
||||
}
|
||||
72
app/Http/Controllers/Transactions/PrechecksController.php
Normal file
72
app/Http/Controllers/Transactions/PrechecksController.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Transactions;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Precheck;
|
||||
use App\Models\Transaction;
|
||||
|
||||
class PrechecksController extends Controller
|
||||
{
|
||||
public function index(Transaction $transaction)
|
||||
{
|
||||
$acConditions = Precheck::getAcConditionOptions();
|
||||
$blowerConditions = Precheck::getBlowerConditionOptions();
|
||||
$evaporatorConditions = Precheck::getEvaporatorConditionOptions();
|
||||
$compressorConditions = Precheck::getCompressorConditionOptions();
|
||||
|
||||
return view('transaction.prechecks', compact(
|
||||
'transaction',
|
||||
'acConditions',
|
||||
'blowerConditions',
|
||||
'evaporatorConditions',
|
||||
'compressorConditions'
|
||||
));
|
||||
}
|
||||
|
||||
public function store(Request $request, Transaction $transaction)
|
||||
{
|
||||
$request->validate([
|
||||
'kilometer' => 'required|numeric|min:0',
|
||||
'pressure_high' => 'required|numeric|min:0',
|
||||
'pressure_low' => 'nullable|numeric|min:0',
|
||||
'cabin_temperature' => 'nullable|numeric',
|
||||
'cabin_temperature_image' => 'nullable|string',
|
||||
'ac_condition' => 'nullable|in:' . implode(',', Precheck::getAcConditionOptions()),
|
||||
'ac_image' => 'nullable|string',
|
||||
'blower_condition' => 'nullable|in:' . implode(',', Precheck::getBlowerConditionOptions()),
|
||||
'blower_image' => 'nullable|string',
|
||||
'evaporator_condition' => 'nullable|in:' . implode(',', Precheck::getEvaporatorConditionOptions()),
|
||||
'evaporator_image' => 'nullable|string',
|
||||
'compressor_condition' => 'nullable|in:' . implode(',', Precheck::getCompressorConditionOptions()),
|
||||
'precheck_notes' => 'nullable|string',
|
||||
'front_image' => 'required|string',
|
||||
]);
|
||||
|
||||
// Pastikan transaction_id sama dengan $transaction->id
|
||||
$precheck = Precheck::create([
|
||||
'transaction_id' => $transaction->id,
|
||||
'precheck_by' => auth()->id(),
|
||||
'precheck_at' => now(),
|
||||
'police_number' => $transaction->police_number,
|
||||
'spk_number' => $transaction->spk,
|
||||
'front_image' => $request->front_image,
|
||||
'kilometer' => $request->kilometer,
|
||||
'pressure_high' => $request->pressure_high,
|
||||
'pressure_low' => $request->pressure_low,
|
||||
'cabin_temperature' => $request->cabin_temperature,
|
||||
'cabin_temperature_image' => $request->cabin_temperature_image,
|
||||
'ac_condition' => $request->ac_condition,
|
||||
'ac_image' => $request->ac_image,
|
||||
'blower_condition' => $request->blower_condition,
|
||||
'blower_image' => $request->blower_image,
|
||||
'evaporator_condition' => $request->evaporator_condition,
|
||||
'evaporator_image' => $request->evaporator_image,
|
||||
'compressor_condition' => $request->compressor_condition,
|
||||
'precheck_notes' => $request->precheck_notes,
|
||||
]);
|
||||
|
||||
return redirect()->route('transaction')->with('success', 'Precheck berhasil disimpan');
|
||||
}
|
||||
}
|
||||
138
app/Models/Postcheck.php
Normal file
138
app/Models/Postcheck.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Postcheck extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'transaction_id',
|
||||
'postcheck_by',
|
||||
'postcheck_at',
|
||||
'police_number',
|
||||
'spk_number',
|
||||
'front_image',
|
||||
'kilometer',
|
||||
'pressure_high',
|
||||
'pressure_low',
|
||||
'cabin_temperature',
|
||||
'cabin_temperature_image',
|
||||
'ac_condition',
|
||||
'ac_image',
|
||||
'blower_condition',
|
||||
'blower_image',
|
||||
'evaporator_condition',
|
||||
'evaporator_image',
|
||||
'compressor_condition',
|
||||
'postcheck_notes'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'postcheck_at' => 'datetime',
|
||||
'kilometer' => 'decimal:2',
|
||||
'pressure_high' => 'decimal:2',
|
||||
'pressure_low' => 'decimal:2',
|
||||
'cabin_temperature' => 'decimal:2',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the transaction associated with the Postcheck
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function transaction()
|
||||
{
|
||||
return $this->belongsTo(Transaction::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who performed the postcheck
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function postcheckBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'postcheck_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AC condition options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getAcConditionOptions()
|
||||
{
|
||||
return ['sudah dikerjakan', 'sudah diganti'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the blower condition options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getBlowerConditionOptions()
|
||||
{
|
||||
return ['sudah dibersihkan atau dicuci', 'sudah diganti'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the evaporator condition options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getEvaporatorConditionOptions()
|
||||
{
|
||||
return ['sudah dikerjakan', 'sudah diganti'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the compressor condition options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getCompressorConditionOptions()
|
||||
{
|
||||
return ['sudah dikerjakan', 'sudah diganti'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter by transaction
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param int $transactionId
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeByTransaction($query, $transactionId)
|
||||
{
|
||||
return $query->where('transaction_id', $transactionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter by user who performed postcheck
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param int $userId
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeByUser($query, $userId)
|
||||
{
|
||||
return $query->where('postcheck_by', $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter by date range
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $startDate
|
||||
* @param string $endDate
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeByDateRange($query, $startDate, $endDate)
|
||||
{
|
||||
return $query->whereBetween('postcheck_at', [$startDate, $endDate]);
|
||||
}
|
||||
}
|
||||
138
app/Models/Precheck.php
Normal file
138
app/Models/Precheck.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Precheck extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'transaction_id',
|
||||
'precheck_by',
|
||||
'precheck_at',
|
||||
'police_number',
|
||||
'spk_number',
|
||||
'front_image',
|
||||
'kilometer',
|
||||
'pressure_high',
|
||||
'pressure_low',
|
||||
'cabin_temperature',
|
||||
'cabin_temperature_image',
|
||||
'ac_condition',
|
||||
'ac_image',
|
||||
'blower_condition',
|
||||
'blower_image',
|
||||
'evaporator_condition',
|
||||
'evaporator_image',
|
||||
'compressor_condition',
|
||||
'precheck_notes'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'precheck_at' => 'datetime',
|
||||
'kilometer' => 'decimal:2',
|
||||
'pressure_high' => 'decimal:2',
|
||||
'pressure_low' => 'decimal:2',
|
||||
'cabin_temperature' => 'decimal:2',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the transaction associated with the Precheck
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function transaction()
|
||||
{
|
||||
return $this->belongsTo(Transaction::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who performed the precheck
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function precheckBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'precheck_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AC condition options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getAcConditionOptions()
|
||||
{
|
||||
return ['kotor', 'rusak', 'baik', 'tidak ada'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the blower condition options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getBlowerConditionOptions()
|
||||
{
|
||||
return ['kotor', 'rusak', 'baik', 'tidak ada'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the evaporator condition options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getEvaporatorConditionOptions()
|
||||
{
|
||||
return ['kotor', 'berlendir', 'bocor', 'bersih'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the compressor condition options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getCompressorConditionOptions()
|
||||
{
|
||||
return ['kotor', 'rusak', 'baik', 'tidak ada'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter by transaction
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param int $transactionId
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeByTransaction($query, $transactionId)
|
||||
{
|
||||
return $query->where('transaction_id', $transactionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter by user who performed precheck
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param int $userId
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeByUser($query, $userId)
|
||||
{
|
||||
return $query->where('precheck_by', $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter by date range
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $startDate
|
||||
* @param string $endDate
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeByDateRange($query, $startDate, $endDate)
|
||||
{
|
||||
return $query->whereBetween('precheck_at', [$startDate, $endDate]);
|
||||
}
|
||||
}
|
||||
@@ -57,4 +57,24 @@ class Transaction extends Model
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_sa_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the precheck associated with the transaction
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
*/
|
||||
public function precheck()
|
||||
{
|
||||
return $this->hasOne(Precheck::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the postcheck associated with the transaction
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
*/
|
||||
public function postcheck()
|
||||
{
|
||||
return $this->hasOne(Postcheck::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,4 +301,24 @@ class User extends Authenticatable
|
||||
->where('privileges.view', 1)
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all prechecks performed by this user
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function prechecks()
|
||||
{
|
||||
return $this->hasMany(Precheck::class, 'precheck_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all postchecks performed by this user
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function postchecks()
|
||||
{
|
||||
return $this->hasMany(Postcheck::class, 'postcheck_by');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreatePrechecksTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('prechecks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('transaction_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('precheck_by')->constrained('users')->onDelete('cascade');
|
||||
$table->timestamp('precheck_at')->nullable();
|
||||
$table->string('police_number');
|
||||
$table->string('spk_number');
|
||||
$table->string('front_image');
|
||||
$table->decimal('kilometer', 10, 2);
|
||||
$table->decimal('pressure_high', 10, 2);
|
||||
$table->decimal('pressure_low', 10, 2)->nullable();
|
||||
$table->decimal('cabin_temperature', 10, 2)->nullable();
|
||||
$table->string('cabin_temperature_image')->nullable();
|
||||
$table->enum('ac_condition', ['kotor', 'rusak', 'baik', 'tidak ada'])->nullable();
|
||||
$table->string('ac_image')->nullable();
|
||||
$table->enum('blower_condition', ['kotor', 'rusak', 'baik', 'tidak ada'])->nullable();
|
||||
$table->string('blower_image')->nullable();
|
||||
$table->enum('evaporator_condition', ['kotor', 'berlendir', 'bocor', 'bersih'])->nullable();
|
||||
$table->string('evaporator_image')->nullable();
|
||||
$table->enum('compressor_condition', ['kotor', 'rusak', 'baik', 'tidak ada'])->nullable();
|
||||
$table->text('precheck_notes')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['transaction_id']);
|
||||
$table->index(['precheck_by']);
|
||||
$table->index(['precheck_at']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('prechecks');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreatePostchecksTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('postchecks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('transaction_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('postcheck_by')->constrained('users')->onDelete('cascade');
|
||||
$table->timestamp('postcheck_at')->nullable();
|
||||
$table->string('police_number');
|
||||
$table->string('spk_number');
|
||||
$table->string('front_image');
|
||||
$table->decimal('kilometer', 10, 2);
|
||||
$table->decimal('pressure_high', 10, 2);
|
||||
$table->decimal('pressure_low', 10, 2)->nullable();
|
||||
$table->decimal('cabin_temperature', 10, 2)->nullable();
|
||||
$table->string('cabin_temperature_image')->nullable();
|
||||
$table->enum('ac_condition', ['sudah dikerjakan', 'sudah diganti'])->nullable();
|
||||
$table->string('ac_image')->nullable();
|
||||
$table->enum('blower_condition', ['sudah dibersihkan atau dicuci', 'sudah diganti'])->nullable();
|
||||
$table->string('blower_image')->nullable();
|
||||
$table->enum('evaporator_condition', ['sudah dikerjakan', 'sudah diganti'])->nullable();
|
||||
$table->string('evaporator_image')->nullable();
|
||||
$table->enum('compressor_condition', ['sudah dikerjakan', 'sudah diganti'])->nullable();
|
||||
$table->text('postcheck_notes')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['transaction_id']);
|
||||
$table->index(['postcheck_by']);
|
||||
$table->index(['postcheck_at']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('postchecks');
|
||||
}
|
||||
}
|
||||
768
resources/views/transaction/postchecks.blade.php
Normal file
768
resources/views/transaction/postchecks.blade.php
Normal file
@@ -0,0 +1,768 @@
|
||||
@extends('layouts.frontapp')
|
||||
|
||||
@section('styles')
|
||||
<style>
|
||||
.card {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
background: linear-gradient(135deg, #2c5aa0 0%, #1e3a8a 100%);
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
margin: 25px 0 15px 0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.section-header h5 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section-header i {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 12px 15px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #2c5aa0;
|
||||
box-shadow: 0 0 0 0.2rem rgba(44, 90, 160, 0.25);
|
||||
}
|
||||
|
||||
.form-control[readonly] {
|
||||
background-color: #f8f9fa;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.camera-container {
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.camera-video {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
height: 250px;
|
||||
background: #000;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.camera-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #2c5aa0 0%, #1e3a8a 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, #059669 0%, #10b981 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, #6c757d 0%, #495057 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 15px 30px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.photo-preview {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.photo-preview img {
|
||||
max-width: 200px;
|
||||
max-height: 150px;
|
||||
border-radius: 8px;
|
||||
border: 3px solid #059669;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
#postcheck_notes {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
/* Mobile Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: 10px 15px;
|
||||
margin: 20px 0 10px 0;
|
||||
}
|
||||
|
||||
.section-header h5 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
padding: 10px 12px;
|
||||
font-size: 16px; /* Prevent zoom on iOS */
|
||||
}
|
||||
|
||||
.camera-video {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.camera-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.photo-preview img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-row > .col-md-6 {
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.mobile-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.camera-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.camera-video {
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="mobile-container">
|
||||
<div class="container">
|
||||
<div class="row mb-4 mt-4">
|
||||
<div class="col-8">
|
||||
<a href="/"><img src="{{ asset('logo-ckb.png') }}" style="width: 100%" alt="LOGO CKB"></a>
|
||||
</div>
|
||||
<div class="col-4 text-right my-auto">
|
||||
<a class="btn btn-sm btn-danger mt-3" style="background: red !important;" href="{{ route('logout') }}" onclick="logout(event)">
|
||||
{{ __('Logout') }}
|
||||
</a>
|
||||
<form id="logout-form" action="{{ route('logout') }}" method="POST" class="d-none">
|
||||
@csrf
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h2 class="text-center">Form Postcheck</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
{{ session('success') }}
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($errors->any())
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<ul class="mb-0">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a href="/" class="btn btn-warning btn-sm">
|
||||
Kembali
|
||||
</a>
|
||||
<form action="{{ route('postchecks.store', $transaction->id) }}" method="POST" id="postcheckForm">
|
||||
@csrf
|
||||
<input type="hidden" name="transaction_id" value="{{ $transaction->id }}">
|
||||
|
||||
<!-- Informasi Transaksi -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-info-circle"></i> Informasi Transaksi</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Nomor Polisi</label>
|
||||
<input type="text" class="form-control" value="{{ $transaction->police_number }}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Nomor SPK</label>
|
||||
<input type="text" class="form-control" value="{{ $transaction->spk }}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Dasar -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-clipboard-list"></i> Data Dasar</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Tanggal Postcheck</label>
|
||||
<input type="text" class="form-control" value="{{ now()->format('d/m/Y H:i') }}" readonly>
|
||||
<small class="form-text text-muted">Tanggal dan waktu saat ini akan digunakan</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="kilometer">Kilometer <span class="text-danger">*</span></label>
|
||||
<input type="number" class="form-control" id="kilometer" name="kilometer" step="0.01" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="pressure_high">Pressure High (PSI) <span class="text-danger">*</span></label>
|
||||
<input type="number" class="form-control" id="pressure_high" name="pressure_high" step="0.01" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="pressure_low">Pressure Low (PSI)</label>
|
||||
<input type="number" class="form-control" id="pressure_low" name="pressure_low" step="0.01">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Foto Depan -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-camera"></i> Foto Depan Kendaraan <span class="text-danger">*</span></h5>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="hidden" id="front_image" name="front_image" required>
|
||||
<div class="camera-container">
|
||||
<video id="front_camera" autoplay playsinline class="camera-video"></video>
|
||||
<canvas id="front_canvas" style="display: none;"></canvas>
|
||||
<div class="camera-controls">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('front_camera')">
|
||||
<i class="fas fa-camera"></i> Buka Kamera
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('front_camera', 'front_canvas', 'front_image', 'front_preview')">
|
||||
<i class="fas fa-camera-retro"></i> Ambil Foto
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Atau upload foto dari galeri:</small>
|
||||
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'front_image', 'front_preview')">
|
||||
</div>
|
||||
<div id="front_preview" class="photo-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Foto Suhu Kabin -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-thermometer-half"></i> Foto Suhu Kabin</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cabin_temperature">Suhu Kabin (°C)</label>
|
||||
<input type="number" class="form-control" id="cabin_temperature" name="cabin_temperature" step="0.1" placeholder="Masukkan suhu kabin">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cabin_temperature_image">Foto Suhu Kabin</label>
|
||||
<input type="hidden" id="cabin_temperature_image" name="cabin_temperature_image">
|
||||
<div class="camera-container">
|
||||
<video id="cabin_camera" autoplay playsinline class="camera-video"></video>
|
||||
<canvas id="cabin_canvas" style="display: none;"></canvas>
|
||||
<div class="camera-controls">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('cabin_camera')">
|
||||
<i class="fas fa-camera"></i> Buka Kamera
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('cabin_camera', 'cabin_canvas', 'cabin_temperature_image', 'cabin_preview')">
|
||||
<i class="fas fa-camera-retro"></i> Ambil Foto
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Atau upload foto dari galeri:</small>
|
||||
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'cabin_temperature_image', 'cabin_preview')">
|
||||
</div>
|
||||
<div id="cabin_preview" class="photo-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kondisi AC -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-snowflake"></i> Kondisi AC</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="ac_condition">Kondisi AC</label>
|
||||
<select class="form-control" id="ac_condition" name="ac_condition">
|
||||
<option value="">Pilih Kondisi</option>
|
||||
@foreach($acConditions as $condition)
|
||||
<option value="{{ $condition }}">{{ ucfirst($condition) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="ac_image">Foto AC</label>
|
||||
<input type="hidden" id="ac_image" name="ac_image">
|
||||
<div class="camera-container">
|
||||
<video id="ac_camera" autoplay playsinline class="camera-video"></video>
|
||||
<canvas id="ac_canvas" style="display: none;"></canvas>
|
||||
<div class="camera-controls">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('ac_camera')">
|
||||
<i class="fas fa-camera"></i> Buka Kamera
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('ac_camera', 'ac_canvas', 'ac_image', 'ac_preview')">
|
||||
<i class="fas fa-camera-retro"></i> Ambil Foto
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Atau upload foto dari galeri:</small>
|
||||
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'ac_image', 'ac_preview')">
|
||||
</div>
|
||||
<div id="ac_preview" class="photo-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kondisi Blower -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-fan"></i> Kondisi Blower</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="blower_condition">Kondisi Blower</label>
|
||||
<select class="form-control" id="blower_condition" name="blower_condition">
|
||||
<option value="">Pilih Kondisi</option>
|
||||
@foreach($blowerConditions as $condition)
|
||||
<option value="{{ $condition }}">{{ ucfirst($condition) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="blower_image">Foto Blower</label>
|
||||
<input type="hidden" id="blower_image" name="blower_image">
|
||||
<div class="camera-container">
|
||||
<video id="blower_camera" autoplay playsinline class="camera-video"></video>
|
||||
<canvas id="blower_canvas" style="display: none;"></canvas>
|
||||
<div class="camera-controls">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('blower_camera')">
|
||||
<i class="fas fa-camera"></i> Buka Kamera
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('blower_camera', 'blower_canvas', 'blower_image', 'blower_preview')">
|
||||
<i class="fas fa-camera-retro"></i> Ambil Foto
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Atau upload foto dari galeri:</small>
|
||||
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'blower_image', 'blower_preview')">
|
||||
</div>
|
||||
<div id="blower_preview" class="photo-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kondisi Evaporator -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-tint"></i> Kondisi Evaporator</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="evaporator_condition">Kondisi Evaporator</label>
|
||||
<select class="form-control" id="evaporator_condition" name="evaporator_condition">
|
||||
<option value="">Pilih Kondisi</option>
|
||||
@foreach($evaporatorConditions as $condition)
|
||||
<option value="{{ $condition }}">{{ ucfirst($condition) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="evaporator_image">Foto Evaporator</label>
|
||||
<input type="hidden" id="evaporator_image" name="evaporator_image">
|
||||
<div class="camera-container">
|
||||
<video id="evaporator_camera" autoplay playsinline class="camera-video"></video>
|
||||
<canvas id="evaporator_canvas" style="display: none;"></canvas>
|
||||
<div class="camera-controls">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('evaporator_camera')">
|
||||
<i class="fas fa-camera"></i> Buka Kamera
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('evaporator_camera', 'evaporator_canvas', 'evaporator_image', 'evaporator_preview')">
|
||||
<i class="fas fa-camera-retro"></i> Ambil Foto
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Atau upload foto dari galeri:</small>
|
||||
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'evaporator_image', 'evaporator_preview')">
|
||||
</div>
|
||||
<div id="evaporator_preview" class="photo-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kondisi Compressor -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-cogs"></i> Kondisi Compressor</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="compressor_condition">Kondisi Compressor</label>
|
||||
<select class="form-control" id="compressor_condition" name="compressor_condition">
|
||||
<option value="">Pilih Kondisi</option>
|
||||
@foreach($compressorConditions as $condition)
|
||||
<option value="{{ $condition }}">{{ ucfirst($condition) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="postcheck_notes">Catatan Tambahan</label>
|
||||
<textarea class="form-control" id="postcheck_notes" name="postcheck_notes" rows="3" placeholder="Masukkan catatan tambahan jika ada..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tombol Submit -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-save"></i> Simpan Data</h5>
|
||||
</div>
|
||||
<div class="form-group text-center">
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
Simpan Postcheck
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('javascripts')
|
||||
<script>
|
||||
let streams = {};
|
||||
|
||||
// Logout function
|
||||
function logout(event){
|
||||
event.preventDefault();
|
||||
Swal.fire({
|
||||
title: 'Logout?',
|
||||
text: "Anda akan keluar dari sistem!",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#d33',
|
||||
cancelButtonColor: '#dedede',
|
||||
confirmButtonText: 'Logout'
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
$('#logout-form').submit();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Fallback untuk browser lama
|
||||
if (navigator.mediaDevices === undefined) {
|
||||
navigator.mediaDevices = {};
|
||||
}
|
||||
|
||||
if (navigator.mediaDevices.getUserMedia === undefined) {
|
||||
navigator.mediaDevices.getUserMedia = function(constraints) {
|
||||
// Coba berbagai versi getUserMedia
|
||||
const getUserMedia = navigator.webkitGetUserMedia ||
|
||||
navigator.mozGetUserMedia ||
|
||||
navigator.msGetUserMedia ||
|
||||
navigator.oGetUserMedia;
|
||||
|
||||
if (!getUserMedia) {
|
||||
return Promise.reject(new Error('getUserMedia tidak didukung di browser ini'));
|
||||
}
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
getUserMedia.call(navigator, constraints, resolve, reject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Tambahan fallback untuk browser yang sangat lama
|
||||
if (navigator.getUserMedia === undefined) {
|
||||
navigator.getUserMedia = navigator.mediaDevices.getUserMedia;
|
||||
}
|
||||
|
||||
// Start camera
|
||||
async function startCamera(videoId) {
|
||||
try {
|
||||
const video = document.getElementById(videoId);
|
||||
|
||||
// Cek apakah browser mendukung getUserMedia
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
throw new Error('Browser tidak mendukung akses kamera');
|
||||
}
|
||||
|
||||
// Stop stream yang sedang berjalan
|
||||
if (streams[videoId]) {
|
||||
streams[videoId].getTracks().forEach(track => track.stop());
|
||||
}
|
||||
|
||||
// Konfigurasi kamera
|
||||
const constraints = {
|
||||
video: {
|
||||
width: { min: 320, ideal: 640, max: 1280 },
|
||||
height: { min: 240, ideal: 480, max: 720 },
|
||||
aspectRatio: { ideal: 4/3 }
|
||||
}
|
||||
};
|
||||
|
||||
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
|
||||
video.srcObject = stream;
|
||||
streams[videoId] = stream;
|
||||
|
||||
// Tunggu video siap
|
||||
video.onloadedmetadata = function() {
|
||||
video.play();
|
||||
};
|
||||
|
||||
video.onerror = function(e) {
|
||||
console.error('Error pada video:', e);
|
||||
alert('Error pada video stream');
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
// Pesan error yang lebih spesifik
|
||||
let errorMessage = 'Tidak dapat mengakses kamera. ';
|
||||
|
||||
if (err.name === 'NotAllowedError') {
|
||||
errorMessage += 'Izin kamera ditolak. Silakan:\n1. Klik ikon kamera di address bar\n2. Pilih "Allow"\n3. Refresh halaman';
|
||||
} else if (err.name === 'NotFoundError') {
|
||||
errorMessage += 'Kamera tidak ditemukan. Pastikan HP memiliki kamera.';
|
||||
} else if (err.name === 'NotReadableError') {
|
||||
errorMessage += 'Kamera sedang digunakan aplikasi lain. Tutup aplikasi kamera lain.';
|
||||
} else if (err.name === 'OverconstrainedError') {
|
||||
errorMessage += 'Kamera tidak mendukung resolusi yang diminta.';
|
||||
} else if (err.name === 'SecurityError') {
|
||||
errorMessage += 'Akses kamera diblokir. Pastikan menggunakan HTTPS atau localhost.';
|
||||
} else {
|
||||
errorMessage += 'Error: ' + err.message;
|
||||
}
|
||||
|
||||
alert(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Capture photo
|
||||
function capturePhoto(videoId, canvasId, inputId, previewId) {
|
||||
const video = document.getElementById(videoId);
|
||||
const canvas = document.getElementById(canvasId);
|
||||
const input = document.getElementById(inputId);
|
||||
const preview = document.getElementById(previewId);
|
||||
|
||||
if (!video.srcObject) {
|
||||
alert('Silakan buka kamera terlebih dahulu');
|
||||
return;
|
||||
}
|
||||
|
||||
// Pastikan video sudah siap
|
||||
if (video.videoWidth === 0 || video.videoHeight === 0) {
|
||||
alert('Video belum siap. Tunggu sebentar dan coba lagi.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
const imageData = canvas.toDataURL('image/jpeg', 0.8);
|
||||
input.value = imageData;
|
||||
|
||||
preview.innerHTML = `
|
||||
<img src="${imageData}" style="max-width: 200px; max-height: 150px; border-radius: 8px; border: 3px solid #059669; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<div class="mt-2">
|
||||
<small class="text-success"><i class="fas fa-check"></i> Foto berhasil diambil</small>
|
||||
</div>
|
||||
`;
|
||||
} catch (err) {
|
||||
alert('Gagal mengambil foto: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Handle file upload
|
||||
function handleFileUpload(input, inputId, previewId) {
|
||||
const file = input.files[0];
|
||||
if (!file) return;
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
alert('Pilih file gambar');
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
const imageData = e.target.result;
|
||||
document.getElementById(inputId).value = imageData;
|
||||
|
||||
const preview = document.getElementById(previewId);
|
||||
preview.innerHTML = `
|
||||
<img src="${imageData}" style="max-width: 200px; max-height: 150px; border-radius: 8px; border: 3px solid #059669; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<div class="mt-2">
|
||||
<small class="text-success"><i class="fas fa-check"></i> Foto berhasil diupload</small>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
// Stop all cameras when page is unloaded
|
||||
window.addEventListener('beforeunload', function() {
|
||||
Object.values(streams).forEach(stream => {
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
});
|
||||
});
|
||||
|
||||
// Form validation
|
||||
document.getElementById('postcheckForm').addEventListener('submit', function(e) {
|
||||
const requiredFields = ['kilometer', 'front_image', 'pressure_high'];
|
||||
let isValid = true;
|
||||
|
||||
requiredFields.forEach(fieldId => {
|
||||
const field = document.getElementById(fieldId);
|
||||
if (!field.value.trim()) {
|
||||
field.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
field.classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
e.preventDefault();
|
||||
alert('Mohon lengkapi semua field yang wajib diisi');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
767
resources/views/transaction/prechecks.blade.php
Normal file
767
resources/views/transaction/prechecks.blade.php
Normal file
@@ -0,0 +1,767 @@
|
||||
@extends('layouts.frontapp')
|
||||
|
||||
@section('styles')
|
||||
<style>
|
||||
.card {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
background: linear-gradient(135deg, #2c5aa0 0%, #1e3a8a 100%);
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
margin: 25px 0 15px 0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.section-header h5 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section-header i {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 12px 15px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #2c5aa0;
|
||||
box-shadow: 0 0 0 0.2rem rgba(44, 90, 160, 0.25);
|
||||
}
|
||||
|
||||
.form-control[readonly] {
|
||||
background-color: #f8f9fa;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.camera-container {
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.camera-video {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
height: 250px;
|
||||
background: #000;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.camera-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #2c5aa0 0%, #1e3a8a 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, #059669 0%, #10b981 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, #6c757d 0%, #495057 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 15px 30px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.photo-preview {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.photo-preview img {
|
||||
max-width: 200px;
|
||||
max-height: 150px;
|
||||
border-radius: 8px;
|
||||
border: 3px solid #059669;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
#precheck_notes {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
/* Mobile Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: 10px 15px;
|
||||
margin: 20px 0 10px 0;
|
||||
}
|
||||
|
||||
.section-header h5 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
padding: 10px 12px;
|
||||
font-size: 16px; /* Prevent zoom on iOS */
|
||||
}
|
||||
|
||||
.camera-video {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.camera-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.photo-preview img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-row > .col-md-6 {
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.mobile-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.camera-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.camera-video {
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="mobile-container">
|
||||
<div class="container">
|
||||
<div class="row mb-4 mt-4">
|
||||
<div class="col-8">
|
||||
<a href="/"><img src="{{ asset('logo-ckb.png') }}" style="width: 100%" alt="LOGO CKB"></a>
|
||||
</div>
|
||||
<div class="col-4 text-right my-auto">
|
||||
<a class="btn btn-sm btn-danger mt-3" style="background: red !important;" href="{{ route('logout') }}" onclick="logout(event)">
|
||||
{{ __('Logout') }}
|
||||
</a>
|
||||
<form id="logout-form" action="{{ route('logout') }}" method="POST" class="d-none">
|
||||
@csrf
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h2 class="text-center">Form Precheck</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
{{ session('success') }}
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($errors->any())
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<ul class="mb-0">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a href="/" class="btn btn-warning btn-sm">
|
||||
Kembali
|
||||
</a>
|
||||
<form action="{{ route('prechecks.store', $transaction->id) }}" method="POST" id="precheckForm">
|
||||
@csrf
|
||||
<input type="hidden" name="transaction_id" value="{{ $transaction->id }}">
|
||||
|
||||
<!-- Informasi Transaksi -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-info-circle"></i> Informasi Transaksi</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Nomor Polisi</label>
|
||||
<input type="text" class="form-control" value="{{ $transaction->police_number }}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Nomor SPK</label>
|
||||
<input type="text" class="form-control" value="{{ $transaction->spk }}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Dasar -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-clipboard-list"></i> Data Dasar</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Tanggal Precheck</label>
|
||||
<input type="text" class="form-control" value="{{ now()->format('d/m/Y H:i') }}" readonly>
|
||||
<small class="form-text text-muted">Tanggal dan waktu saat ini akan digunakan</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="kilometer">Kilometer <span class="text-danger">*</span></label>
|
||||
<input type="number" class="form-control" id="kilometer" name="kilometer" step="0.01" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="pressure_high">Pressure High (PSI) <span class="text-danger">*</span></label>
|
||||
<input type="number" class="form-control" id="pressure_high" name="pressure_high" step="0.01" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="pressure_low">Pressure Low (PSI)</label>
|
||||
<input type="number" class="form-control" id="pressure_low" name="pressure_low" step="0.01">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Foto Depan -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-camera"></i> Foto Depan Kendaraan <span class="text-danger">*</span></h5>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="hidden" id="front_image" name="front_image" required>
|
||||
<div class="camera-container">
|
||||
<video id="front_camera" autoplay playsinline class="camera-video"></video>
|
||||
<canvas id="front_canvas" style="display: none;"></canvas>
|
||||
<div class="camera-controls">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('front_camera')">
|
||||
<i class="fas fa-camera"></i> Buka Kamera
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('front_camera', 'front_canvas', 'front_image', 'front_preview')">
|
||||
<i class="fas fa-camera-retro"></i> Ambil Foto
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Atau upload foto dari galeri:</small>
|
||||
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'front_image', 'front_preview')">
|
||||
</div>
|
||||
<div id="front_preview" class="photo-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Foto Suhu Kabin -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-thermometer-half"></i> Foto Suhu Kabin</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cabin_temperature">Suhu Kabin (°C)</label>
|
||||
<input type="number" class="form-control" id="cabin_temperature" name="cabin_temperature" step="0.1" placeholder="Masukkan suhu kabin">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cabin_temperature_image">Foto Suhu Kabin</label>
|
||||
<input type="hidden" id="cabin_temperature_image" name="cabin_temperature_image">
|
||||
<div class="camera-container">
|
||||
<video id="cabin_camera" autoplay playsinline class="camera-video"></video>
|
||||
<canvas id="cabin_canvas" style="display: none;"></canvas>
|
||||
<div class="camera-controls">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('cabin_camera')">
|
||||
<i class="fas fa-camera"></i> Buka Kamera
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('cabin_camera', 'cabin_canvas', 'cabin_temperature_image', 'cabin_preview')">
|
||||
<i class="fas fa-camera-retro"></i> Ambil Foto
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Atau upload foto dari galeri:</small>
|
||||
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'cabin_temperature_image', 'cabin_preview')">
|
||||
</div>
|
||||
<div id="cabin_preview" class="photo-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kondisi AC -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-snowflake"></i> Kondisi AC</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="ac_condition">Kondisi AC</label>
|
||||
<select class="form-control" id="ac_condition" name="ac_condition">
|
||||
<option value="">Pilih Kondisi</option>
|
||||
@foreach($acConditions as $condition)
|
||||
<option value="{{ $condition }}">{{ ucfirst($condition) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="ac_image">Foto AC</label>
|
||||
<input type="hidden" id="ac_image" name="ac_image">
|
||||
<div class="camera-container">
|
||||
<video id="ac_camera" autoplay playsinline class="camera-video"></video>
|
||||
<canvas id="ac_canvas" style="display: none;"></canvas>
|
||||
<div class="camera-controls">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('ac_camera')">
|
||||
<i class="fas fa-camera"></i> Buka Kamera
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('ac_camera', 'ac_canvas', 'ac_image', 'ac_preview')">
|
||||
<i class="fas fa-camera-retro"></i> Ambil Foto
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Atau upload foto dari galeri:</small>
|
||||
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'ac_image', 'ac_preview')">
|
||||
</div>
|
||||
<div id="ac_preview" class="photo-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kondisi Blower -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-fan"></i> Kondisi Blower</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="blower_condition">Kondisi Blower</label>
|
||||
<select class="form-control" id="blower_condition" name="blower_condition">
|
||||
<option value="">Pilih Kondisi</option>
|
||||
@foreach($blowerConditions as $condition)
|
||||
<option value="{{ $condition }}">{{ ucfirst($condition) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="blower_image">Foto Blower</label>
|
||||
<input type="hidden" id="blower_image" name="blower_image">
|
||||
<div class="camera-container">
|
||||
<video id="blower_camera" autoplay playsinline class="camera-video"></video>
|
||||
<canvas id="blower_canvas" style="display: none;"></canvas>
|
||||
<div class="camera-controls">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('blower_camera')">
|
||||
<i class="fas fa-camera"></i> Buka Kamera
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('blower_camera', 'blower_canvas', 'blower_image', 'blower_preview')">
|
||||
<i class="fas fa-camera-retro"></i> Ambil Foto
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Atau upload foto dari galeri:</small>
|
||||
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'blower_image', 'blower_preview')">
|
||||
</div>
|
||||
<div id="blower_preview" class="photo-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kondisi Evaporator -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-tint"></i> Kondisi Evaporator</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="evaporator_condition">Kondisi Evaporator</label>
|
||||
<select class="form-control" id="evaporator_condition" name="evaporator_condition">
|
||||
<option value="">Pilih Kondisi</option>
|
||||
@foreach($evaporatorConditions as $condition)
|
||||
<option value="{{ $condition }}">{{ ucfirst($condition) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="evaporator_image">Foto Evaporator</label>
|
||||
<input type="hidden" id="evaporator_image" name="evaporator_image">
|
||||
<div class="camera-container">
|
||||
<video id="evaporator_camera" autoplay playsinline class="camera-video"></video>
|
||||
<canvas id="evaporator_canvas" style="display: none;"></canvas>
|
||||
<div class="camera-controls">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('evaporator_camera')">
|
||||
<i class="fas fa-camera"></i> Buka Kamera
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('evaporator_camera', 'evaporator_canvas', 'evaporator_image', 'evaporator_preview')">
|
||||
<i class="fas fa-camera-retro"></i> Ambil Foto
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Atau upload foto dari galeri:</small>
|
||||
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'evaporator_image', 'evaporator_preview')">
|
||||
</div>
|
||||
<div id="evaporator_preview" class="photo-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kondisi Compressor -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-cogs"></i> Kondisi Compressor</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="compressor_condition">Kondisi Compressor</label>
|
||||
<select class="form-control" id="compressor_condition" name="compressor_condition">
|
||||
<option value="">Pilih Kondisi</option>
|
||||
@foreach($compressorConditions as $condition)
|
||||
<option value="{{ $condition }}">{{ ucfirst($condition) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="precheck_notes">Catatan Tambahan</label>
|
||||
<textarea class="form-control" id="precheck_notes" name="precheck_notes" rows="3" placeholder="Masukkan catatan tambahan jika ada..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tombol Submit -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-save"></i> Simpan Data</h5>
|
||||
</div>
|
||||
<div class="form-group text-center">
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
Simpan Precheck
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('javascripts')
|
||||
<script>
|
||||
let streams = {};
|
||||
|
||||
// Logout function
|
||||
function logout(event){
|
||||
event.preventDefault();
|
||||
Swal.fire({
|
||||
title: 'Logout?',
|
||||
text: "Anda akan keluar dari sistem!",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#d33',
|
||||
cancelButtonColor: '#dedede',
|
||||
confirmButtonText: 'Logout'
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
$('#logout-form').submit();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Fallback untuk browser lama
|
||||
if (navigator.mediaDevices === undefined) {
|
||||
navigator.mediaDevices = {};
|
||||
}
|
||||
|
||||
if (navigator.mediaDevices.getUserMedia === undefined) {
|
||||
navigator.mediaDevices.getUserMedia = function(constraints) {
|
||||
// Coba berbagai versi getUserMedia
|
||||
const getUserMedia = navigator.webkitGetUserMedia ||
|
||||
navigator.mozGetUserMedia ||
|
||||
navigator.msGetUserMedia ||
|
||||
navigator.oGetUserMedia;
|
||||
|
||||
if (!getUserMedia) {
|
||||
return Promise.reject(new Error('getUserMedia tidak didukung di browser ini'));
|
||||
}
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
getUserMedia.call(navigator, constraints, resolve, reject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Tambahan fallback untuk browser yang sangat lama
|
||||
if (navigator.getUserMedia === undefined) {
|
||||
navigator.getUserMedia = navigator.mediaDevices.getUserMedia;
|
||||
}
|
||||
|
||||
// Start camera
|
||||
async function startCamera(videoId) {
|
||||
try {
|
||||
const video = document.getElementById(videoId);
|
||||
|
||||
// Cek apakah browser mendukung getUserMedia
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
throw new Error('Browser tidak mendukung akses kamera');
|
||||
}
|
||||
|
||||
// Stop stream yang sedang berjalan
|
||||
if (streams[videoId]) {
|
||||
streams[videoId].getTracks().forEach(track => track.stop());
|
||||
}
|
||||
|
||||
// Konfigurasi kamera
|
||||
const constraints = {
|
||||
video: {
|
||||
width: { min: 320, ideal: 640, max: 1280 },
|
||||
height: { min: 240, ideal: 480, max: 720 },
|
||||
aspectRatio: { ideal: 4/3 }
|
||||
}
|
||||
};
|
||||
|
||||
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
|
||||
video.srcObject = stream;
|
||||
streams[videoId] = stream;
|
||||
|
||||
// Tunggu video siap
|
||||
video.onloadedmetadata = function() {
|
||||
video.play();
|
||||
};
|
||||
|
||||
video.onerror = function(e) {
|
||||
console.error('Error pada video:', e);
|
||||
alert('Error pada video stream');
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
// Pesan error yang lebih spesifik
|
||||
let errorMessage = 'Tidak dapat mengakses kamera. ';
|
||||
|
||||
if (err.name === 'NotAllowedError') {
|
||||
errorMessage += 'Izin kamera ditolak. Silakan:\n1. Klik ikon kamera di address bar\n2. Pilih "Allow"\n3. Refresh halaman';
|
||||
} else if (err.name === 'NotFoundError') {
|
||||
errorMessage += 'Kamera tidak ditemukan. Pastikan HP memiliki kamera.';
|
||||
} else if (err.name === 'NotReadableError') {
|
||||
errorMessage += 'Kamera sedang digunakan aplikasi lain. Tutup aplikasi kamera lain.';
|
||||
} else if (err.name === 'OverconstrainedError') {
|
||||
errorMessage += 'Kamera tidak mendukung resolusi yang diminta.';
|
||||
} else if (err.name === 'SecurityError') {
|
||||
errorMessage += 'Akses kamera diblokir. Pastikan menggunakan HTTPS atau localhost.';
|
||||
} else {
|
||||
errorMessage += 'Error: ' + err.message;
|
||||
}
|
||||
|
||||
alert(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Capture photo
|
||||
function capturePhoto(videoId, canvasId, inputId, previewId) {
|
||||
const video = document.getElementById(videoId);
|
||||
const canvas = document.getElementById(canvasId);
|
||||
const input = document.getElementById(inputId);
|
||||
const preview = document.getElementById(previewId);
|
||||
|
||||
if (!video.srcObject) {
|
||||
alert('Silakan buka kamera terlebih dahulu');
|
||||
return;
|
||||
}
|
||||
|
||||
// Pastikan video sudah siap
|
||||
if (video.videoWidth === 0 || video.videoHeight === 0) {
|
||||
alert('Video belum siap. Tunggu sebentar dan coba lagi.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
const imageData = canvas.toDataURL('image/jpeg', 0.8);
|
||||
input.value = imageData;
|
||||
|
||||
preview.innerHTML = `
|
||||
<img src="${imageData}" style="max-width: 200px; max-height: 150px; border-radius: 8px; border: 3px solid #059669; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<div class="mt-2">
|
||||
<small class="text-success"><i class="fas fa-check"></i> Foto berhasil diambil</small>
|
||||
</div>
|
||||
`;
|
||||
} catch (err) {
|
||||
alert('Gagal mengambil foto: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Handle file upload
|
||||
function handleFileUpload(input, inputId, previewId) {
|
||||
const file = input.files[0];
|
||||
if (!file) return;
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
alert('Pilih file gambar');
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
const imageData = e.target.result;
|
||||
document.getElementById(inputId).value = imageData;
|
||||
|
||||
const preview = document.getElementById(previewId);
|
||||
preview.innerHTML = `
|
||||
<img src="${imageData}" style="max-width: 200px; max-height: 150px; border-radius: 8px; border: 3px solid #059669; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<div class="mt-2">
|
||||
<small class="text-success"><i class="fas fa-check"></i> Foto berhasil diupload</small>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
// Stop all cameras when page is unloaded
|
||||
window.addEventListener('beforeunload', function() {
|
||||
Object.values(streams).forEach(stream => {
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
});
|
||||
});
|
||||
|
||||
// Form validation
|
||||
document.getElementById('precheckForm').addEventListener('submit', function(e) {
|
||||
const requiredFields = ['kilometer', 'front_image', 'pressure_high'];
|
||||
let isValid = true;
|
||||
|
||||
requiredFields.forEach(fieldId => {
|
||||
const field = document.getElementById(fieldId);
|
||||
if (!field.value.trim()) {
|
||||
field.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
field.classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
e.preventDefault();
|
||||
alert('Mohon lengkapi semua field yang wajib diisi');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@@ -21,6 +21,8 @@ use App\Models\Menu;
|
||||
use App\Models\Privilege;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Http\Controllers\Transactions\PrechecksController;
|
||||
use App\Http\Controllers\Transactions\PostchecksController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@@ -173,6 +175,14 @@ Route::group(['middleware' => 'auth'], function() {
|
||||
// Claim Transactions Route
|
||||
Route::get('/transaction/get-claim-transactions', [TransactionController::class, 'getClaimTransactions'])->name('transaction.get-claim-transactions');
|
||||
Route::post('/transaction/claim/{id}', [TransactionController::class, 'claim'])->name('transaction.claim');
|
||||
|
||||
// Prechecks Routes
|
||||
Route::get('/transaction/prechecks/{transaction}', [PrechecksController::class, 'index'])->name('prechecks.index');
|
||||
Route::post('/transaction/prechecks/{transaction}', [PrechecksController::class, 'store'])->name('prechecks.store');
|
||||
|
||||
// Postchecks Routes
|
||||
Route::get('/transaction/postchecks/{transaction}', [PostchecksController::class, 'index'])->name('postchecks.index');
|
||||
Route::post('/transaction/postchecks/{transaction}', [PostchecksController::class, 'store'])->name('postchecks.store');
|
||||
});
|
||||
|
||||
// KPI Data Route - accessible to all authenticated users
|
||||
|
||||
Reference in New Issue
Block a user