fix form create update postcheck and precheck

This commit is contained in:
2025-09-19 20:44:28 +07:00
parent cab0d2e9a8
commit db4c586535
22 changed files with 4977 additions and 224 deletions

View File

@@ -55,7 +55,6 @@ class AdminController extends Controller
}
$ajax_url = route('dashboard_data').'?month='.$month.'&year='.$year.'&dealer='.$dealer;
// dd($ajax_url);
return view('dashboard', compact('month','year', 'ajax_url', 'dealer', 'dealer_datas'));
}
@@ -134,7 +133,6 @@ class AdminController extends Controller
$dealer_work_trx = DB::statement("PREPARE stmt FROM @sql");
$dealer_work_trx = DB::select(DB::raw("EXECUTE stmt"));
DB::statement('DEALLOCATE PREPARE stmt');
// DD($dealer_work_trx);
$theads = ['DEALER'];
$dealer_names = [];
$dealer_trx = [];
@@ -165,7 +163,6 @@ class AdminController extends Controller
$dealer_names[] = $dealer_work->DEALER;
}
// dd($dealer_trx);
$dealer_trx = array_values($dealer_trx);
$dealer = $request->dealer;
$month = $request->month;
@@ -319,7 +316,6 @@ class AdminController extends Controller
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t', strtotime($prev_mth_start));
}
// dd($prev_mth_end);
$yesterday_month_trx = Transaction::where('work_id', $work1->id)->where('dealer_id', $dealer->id)->whereDate('date', '>=', $prev_mth_start)->whereDate('date', '<=', $prev_mth_end)->sum('qty');
if(array_key_exists($work1->id, $prev_month_trxs_total)) {
@@ -528,16 +524,12 @@ class AdminController extends Controller
// $month_trxs_total = array_values($month_trxs_total);
// $yesterday_month_trxs_total = array_values($yesterday_month_trxs_total);
// dd(["month_trxs_total" => $month_trxs_total, "yesterday_month_trxs_total" => $yesterday_month_trxs_total, "works" => $works->toArray()]);
// dd($month_trxs_total);
// dd($yesterday_month_trxs_total);
$final_month_trxs_total = [];
$final_yesterday_month_trxs_total = [];
foreach($works as $work1) {
$final_month_trxs_total[$work1->id] = array_key_exists($work1->id, $month_trxs_total) ? $month_trxs_total[$work1->id] : 0;
$final_yesterday_month_trxs_total[$work1->id] = $yesterday_month_trxs_total[$work1->id];
}
// dd([$final_month_trxs_total, $final_yesterday_month_trxs_total]);
$month_trxs_total = array_values($final_month_trxs_total);
$yesterday_month_trxs_total = array_values($final_yesterday_month_trxs_total);
$totals = [];

View File

@@ -93,7 +93,6 @@ class ApiController extends Controller
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t');
}
// dd($prev_mth_end);
$yesterday_month_trx = Transaction::where('work_id', $work1->id)->where('dealer_id', $id)->whereDate('date', '>=', $prev_mth_start)->whereDate('date', '<=', $prev_mth_end)->sum('qty');
if(array_key_exists($work1->id, $yesterday_month_trxs_total)) {
@@ -153,7 +152,6 @@ class ApiController extends Controller
$final_month_trxs_total[$work1->id] = $month_trxs_total[$work1->id];
$final_yesterday_month_trxs_total[$work1->id] = $yesterday_month_trxs_total[$work1->id];
}
// dd([$final_month_trxs_total, $final_yesterday_month_trxs_total]);
$month_trxs_total = array_values($final_month_trxs_total);
$yesterday_month_trxs_total = array_values($final_yesterday_month_trxs_total);

View File

@@ -472,7 +472,6 @@ class ReportController extends Controller
$sa_names = json_encode($sa_names);
$trx_data = json_encode(array_values($trx_data));
// dd($trx_data);
$work_count = count($works);
$month = $request->month;
$dealer_id = $request->dealer;
@@ -703,11 +702,28 @@ class ReportController extends Controller
}
$data = Transaction::leftJoin('users', 'users.id', '=', 'transactions.user_id')
->leftJoin('users as sa', 'sa.id', '=', 'transactions.user_sa_id')
->leftJoin('works as w', 'w.id', '=', 'transactions.work_id')
->leftJoin('categories as cat', 'cat.id', '=', 'w.category_id')
->leftJoin('dealers as d', 'd.id', '=', 'transactions.dealer_id')
->select('transactions.id', 'transactions.status', 'transactions.user_id as user_id', 'transactions.user_sa_id as user_sa_id', 'users.name as username', 'sa.name as sa_name', 'cat.name as category_name', 'w.name as workname', 'transactions.qty as qty', 'transactions.date as date', 'transactions.police_number as police_number', 'transactions.warranty as warranty', 'transactions.spk as spk', 'transactions.dealer_id', 'd.name as dealer_name');
->leftJoin('users as sa', 'sa.id', '=', 'transactions.user_sa_id')
->leftJoin('works as w', 'w.id', '=', 'transactions.work_id')
->leftJoin('categories as cat', 'cat.id', '=', 'w.category_id')
->leftJoin('dealers as d', 'd.id', '=', 'transactions.dealer_id')
->leftJoin('prechecks as pre', 'pre.transaction_id', '=', 'transactions.id')
->leftJoin('postchecks as post', 'post.transaction_id', '=', 'transactions.id')
->select(
'transactions.id',
'transactions.status',
'users.name as username',
'sa.name as sa_name',
'cat.name as category_name',
'w.name as workname',
'transactions.qty as qty',
'transactions.date as date',
'transactions.police_number as police_number',
'transactions.warranty as warranty',
'transactions.spk as spk',
'd.name as dealer_name',
DB::raw('pre.id as precheck_id'),
DB::raw('post.id as postcheck_id')
);
// Filter by allowed dealers based on user role
if($allowedDealers->count() > 0) {
@@ -747,24 +763,70 @@ class ReportController extends Controller
$data->orderBy('date', 'DESC');
return DataTables::of($data)->addIndexColumn()
->addColumn('action', function($row) use ($menu) {
$btn = '<div class="d-flex justify-content-center">';
$btn = '<div class="d-flex justify-content-center align-items-center flex-wrap">';
if($row->status == 1) {
if(Gate::allows('delete', $menu)) {
$btn .= ' <button class="btn btn-danger btn-sm btn-bold mr-2" data-action="'. route('report.transaction.destroy', $row->id) .'" id="destroyTransaction'. $row->id .'" onclick="destroyTransaction('. $row->id .')"> Hapus </button>';
// Jika status closed
if ($row->status == 1) {
if (Gate::allows('delete', $menu)) {
$btn .= '<button class="btn btn-danger btn-sm font-weight-bold mr-2 mt-2"
data-action="'. route('report.transaction.destroy', $row->id) .'"
id="destroyTransaction'. $row->id .'"
onclick="destroyTransaction('. $row->id .')">
Hapus
</button>';
}
$btn .= '<span class="badge badge-success">Closed</span>';
}else{
if(Gate::allows('delete', $menu)) {
$btn .= '<button class="btn btn-danger btn-sm btn-bold mr-2" data-action="'. route('report.transaction.destroy', $row->id) .'" id="destroyTransaction'. $row->id .'" onclick="destroyTransaction('. $row->id .')"> Hapus </button>';
// Badge Closed rapi
$btn .= '<span class="btn btn-success btn-sm font-weight-bold px-3 py-2 mr-2 mt-2 disabled"
style="pointer-events: none; cursor: default;">
Closed
</span>';
} else {
if (Gate::allows('delete', $menu)) {
$btn .= '<button class="btn btn-danger btn-sm font-weight-bold mr-2 mt-2"
data-action="'. route('report.transaction.destroy', $row->id) .'"
id="destroyTransaction'. $row->id .'"
onclick="destroyTransaction('. $row->id .')">
Hapus
</button>';
}
if(Gate::allows('update', $menu)) {
$btn .= '<button class="btn btn-info btn-sm btn-bold mr-2" data-url="'. route('report.transaction.edit', $row->id) .'" data-action="'. route('report.transaction.update', $row->id) .'" onclick="editTransaction('. $row->id .')" id="editTransaction'. $row->id .'"> Edit </button>
<button class="btn btn-warning btn-sm btn-bold" id="closeTransaction'. $row->id .'" data-url="'. route('report.transaction.close', $row->id) .'" onclick="closeTransaction('. $row->id .')"> Close </button>';
if (Gate::allows('update', $menu)) {
$btn .= '<button class="btn btn-info btn-sm font-weight-bold mr-2 mt-2"
data-url="'. route('report.transaction.edit', $row->id) .'"
data-action="'. route('report.transaction.update', $row->id) .'"
onclick="editTransaction('. $row->id .')"
id="editTransaction'. $row->id .'">
Edit
</button>';
$btn .= '<button class="btn btn-warning btn-sm font-weight-bold mr-2 mt-2"
id="closeTransaction'. $row->id .'"
data-url="'. route('report.transaction.close', $row->id) .'"
onclick="closeTransaction('. $row->id .')">
Close
</button>';
}
}
if ($row->precheck_id) {
$btn .= '<button class="btn btn-primary btn-sm font-weight-bold action-print mr-2 mt-2"
data-type="precheck"
data-id="'. $row->id .'"
data-url="'. route('report.transaction.precheck.print', $row->id) .'">
Pre Check
</button>';
}
if ($row->postcheck_id) {
$btn .= '<button class="btn btn-success btn-sm font-weight-bold action-print mr-2 mt-2"
data-type="postcheck"
data-id="'. $row->id .'"
data-url="'. route('report.transaction.postcheck.print', $row->id) .'">
Post Check
</button>';
}
$btn .= '</div>';
return $btn;

View File

@@ -15,6 +15,9 @@ use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use App\Models\Precheck;
use App\Models\Postcheck;
use Illuminate\Support\Facades\Log;
use Exception;
class TransactionController extends Controller
@@ -519,7 +522,6 @@ class TransactionController extends Controller
$works_count = count($works);
$share = $month_trxs;
$month = $request->month;
dd($share);
return view('transaction.recap', compact('month_trxs_total', 'yesterday_month_trxs_total', 'month', 'trx_data', 'sa_names', 'works', 'works_count', 'trxs', 'month_trxs','dealer', 'share', 'mechanic'));
}
@@ -568,7 +570,6 @@ class TransactionController extends Controller
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t');
}
// dd($prev_mth_end);
$yesterday_month_trx = Transaction::whereNull('deleted_at')->where('work_id', $work1->id)->where('dealer_id', $id)->whereDate('date', '>=', $prev_mth_start)->whereDate('date', '<=', $prev_mth_end)->sum('qty');
if(array_key_exists($work1->id, $yesterday_month_trxs_total)) {
@@ -678,15 +679,12 @@ class TransactionController extends Controller
}
}
// dd($works);
// dd([$month_trxs_total, $yesterday_month_trxs_total]);
$final_month_trxs_total = [];
$final_yesterday_month_trxs_total = [];
foreach($works as $work1) {
$final_month_trxs_total[$work1->id] = $month_trxs_total[$work1->id];
$final_yesterday_month_trxs_total[$work1->id] = $yesterday_month_trxs_total[$work1->id];
}
// dd([$final_month_trxs_total, $final_yesterday_month_trxs_total]);
$month_trxs_total = array_values($final_month_trxs_total);
$yesterday_month_trxs_total = array_values($final_yesterday_month_trxs_total);
@@ -994,19 +992,31 @@ class TransactionController extends Controller
public function update(Request $request, $id)
{
Transaction::find($id)->update([
$request->validate([
'spk' => 'required|string|max:255',
'date' => 'required|date',
'police_number' => 'required|string|max:255',
'work_id' => 'required|exists:works,id',
'qty' => 'required|integer|min:1',
'warranty' => 'required|in:0,1',
'user_sa_id' => 'required|exists:users,id',
]);
$transaction = Transaction::findOrFail($id);
$transaction->update([
"spk" => $request->spk,
"date" => $request->date,
"police_number" => $request->police_number,
"work_id" => $request->work_id,
"qty" => $request->qty,
"warranty" => $request->warranty,
"user_sa_id" => $request->sa_id,
"user_sa_id" => $request->user_sa_id,
]);
$response = [
"status" => 200,
"message" => "Data updated successfully"
"message" => "Transaksi berhasil diperbarui"
];
return response()->json($response);
@@ -1147,6 +1157,8 @@ class TransactionController extends Controller
'sa_name' => $transaction->sa_name,
'status' => $this->getStatusBadge($transaction->status),
'action' => $this->getActionButtons($transaction),
'action_precheck' => $this->getActionButtonsPrecheck($transaction),
'action_postcheck' => $this->getActionButtonsPostcheck($transaction),
'claimed_at' => $transaction->claimed_at,
'claimed_by' => $transaction->claimed_by
];
@@ -1259,38 +1271,108 @@ class TransactionController extends Controller
{
$buttons = '';
// Only show buttons for mechanics
// Edit button - show for all users (not just mechanics)
$buttons .= '<button class="btn btn-sm btn-warning mr-1"
data-action="' . route('transaction.update', $transaction->id) . '"
data-url="' . route('transaction.edit', $transaction->id) . '"
onclick="editTransaction(' . $transaction->id . ')"
id="editTransaction' . $transaction->id . '"
title="Edit Transaksi"
style="font-size: 11px; padding: 4px 8px;">
<i class="fas fa-edit"></i> Edit
</button>';
// Delete button - show for all users
$buttons .= '<button class="btn btn-sm btn-danger mr-1"
onclick="deleteTransaction(' . $transaction->id . ')"
title="Hapus Transaksi"
style="font-size: 11px; padding: 4px 8px;">
<i class="fas fa-trash"></i> Hapus
</button>';
// Only show claim buttons for mechanics
if (Auth::user()->role_id == 3) {
// Claim button - show only if not claimed yet
if (empty($transaction->claimed_at) && empty($transaction->claimed_by)) {
$buttons .= '<button class="btn btn-sm btn-success mr-1" onclick="claimTransaction(' . $transaction->id . ')" title="Klaim Pekerjaan">';
$buttons .= 'Klaim';
$buttons .= '<button class="btn btn-sm btn-success mr-1" onclick="claimTransaction(' . $transaction->id . ')" title="Klaim Pekerjaan" style="font-size: 11px; padding: 4px 8px;">';
$buttons .= '<i class="fas fa-hand-paper"></i> 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>';
}
if ($transaction->claimed_by == Auth::user()->id) {
$precheck = Precheck::where('transaction_id', $transaction->id)->first();
$postcheck = Postcheck::where('transaction_id', $transaction->id)->first();
if ($precheck && $postcheck) {
$buttons .= '<span class="badge badge-success" style="font-size: 10px;"><i class="fas fa-check"></i> Selesai</span>';
}
}
$buttons .= '<span class="badge badge-info">Sudah Diklaim</span>';
$buttons .= '<span class="badge badge-info" style="font-size: 10px;"><i class="fas fa-check-circle"></i> Sudah Diklaim</span>';
}
}
return $buttons;
}
private function getActionButtonsPrecheck($transaction)
{
$buttons = '';
if (Auth::user()->role_id == 3) {
$precheck = Precheck::where('transaction_id', $transaction->id)->first();
if ($precheck) {
$buttons .= '<a href="' . route('prechecks.edit', [$transaction->id, $precheck->id]) . '"
class="btn btn-sm btn-warning mr-1" title="Edit Precheck" style="font-size: 11px; padding: 4px 8px;">
<i class="fas fa-edit"></i> Edit
</a>';
$buttons .= '<a href="' . route('prechecks.print', $transaction->id) . '"
class="btn btn-sm btn-primary mr-1" title="Lihat Precheck" target="_blank" style="font-size: 11px; padding: 4px 8px;">
<i class="fas fa-eye"></i> Lihat
</a>';
} else {
if (empty($transaction->claimed_at) && empty($transaction->claimed_by)) {
$buttons .= '<span class="badge badge-danger" style="font-size: 10px;">Transaksi Belum Diklaim</span>';
}else{
$buttons .= '<a href="' . route('prechecks.create', $transaction->id) . '"
class="btn btn-sm btn-success mr-1" title="Tambah Precheck" style="font-size: 11px; padding: 4px 8px;">
<i class="fas fa-plus"></i> Tambah
</a>';
}
}
}
return $buttons;
}
private function getActionButtonsPostcheck($transaction)
{
$buttons = '';
if (Auth::user()->role_id == 3) {
$postcheck = Postcheck::where('transaction_id', $transaction->id)->first();
$precheck = Precheck::where('transaction_id', $transaction->id)->first();
if($precheck){
if ($postcheck) {
$buttons .= '<a href="' . route('postchecks.edit', [$transaction->id, $postcheck->id]) . '"
class="btn btn-sm btn-warning mr-1" title="Edit Postcheck" style="font-size: 11px; padding: 4px 8px;">
<i class="fas fa-edit"></i> Edit
</a>';
$buttons .= '<a href="' . route('postchecks.print', $transaction->id) . '"
class="btn btn-sm btn-primary mr-1" title="Lihat Postcheck" target="_blank" style="font-size: 11px; padding: 4px 8px;">
<i class="fas fa-eye"></i> Lihat
</a>';
} else {
$buttons .= '<a href="' . route('postchecks.create', $transaction->id) . '"
class="btn btn-sm btn-success mr-1" title="Tambah Postcheck" style="font-size: 11px; padding: 4px 8px;">
<i class="fas fa-plus"></i> Tambah
</a>';
}
}else{
$buttons .= '<span class="badge badge-danger" style="font-size: 10px;">Precheck Belum Disimpan</span>';
}
}
return $buttons;
}

View File

@@ -11,14 +11,14 @@ use Illuminate\Support\Facades\Log;
class PostchecksController extends Controller
{
public function index(Transaction $transaction)
public function create(Transaction $transaction)
{
$acConditions = Postcheck::getAcConditionOptions();
$blowerConditions = Postcheck::getBlowerConditionOptions();
$evaporatorConditions = Postcheck::getEvaporatorConditionOptions();
$compressorConditions = Postcheck::getCompressorConditionOptions();
return view('transaction.postchecks', compact(
return view('transaction.postchecks.create', compact(
'transaction',
'acConditions',
'blowerConditions',
@@ -62,76 +62,15 @@ class PostchecksController extends Controller
'compressor_condition' => $request->compressor_condition,
'postcheck_notes' => $request->postcheck_notes,
];
// Handle file uploads
// Handle file uploads securely
$imageFields = [
'front_image', 'cabin_temperature_image', 'ac_image',
'front_image', 'cabin_temperature_image', 'ac_image',
'blower_image', 'evaporator_image'
];
foreach ($imageFields as $field) {
if ($request->hasFile($field) && $request->file($field)->isValid()) {
try {
$file = $request->file($field);
// Generate unique filename with transaction ID
$filename = time() . '_' . uniqid() . '_' . $transaction->id . '_' . $field . '.' . $file->getClientOriginalExtension();
// Create directory path: transactions/{transaction_id}/postcheck/
$directory = 'transactions/' . $transaction->id . '/postcheck';
// Ensure base storage directory exists
$this->ensureStorageDirectoryExists();
// Ensure transactions directory exists
if (!Storage::disk('public')->exists('transactions')) {
Storage::disk('public')->makeDirectory('transactions', 0755, true);
Log::info('Created transactions directory');
}
// Ensure transaction ID directory exists
$transactionDir = 'transactions/' . $transaction->id;
if (!Storage::disk('public')->exists($transactionDir)) {
Storage::disk('public')->makeDirectory($transactionDir, 0755, true);
Log::info('Created transaction directory: ' . $transactionDir);
}
// Ensure postcheck directory exists
if (!Storage::disk('public')->exists($directory)) {
Storage::disk('public')->makeDirectory($directory, 0755, true);
Log::info('Created postcheck directory: ' . $directory);
}
// Store file in organized directory structure
$path = $file->storeAs($directory, $filename, 'public');
// Store file path
$data[$field] = $path;
// Store metadata
$data[$field . '_metadata'] = [
'original_name' => $file->getClientOriginalName(),
'size' => $file->getSize(),
'mime_type' => $file->getMimeType(),
'uploaded_at' => now()->toISOString(),
'transaction_id' => $transaction->id,
'filename' => $filename,
];
Log::info('File uploaded successfully: ' . $path);
} catch (\Exception $e) {
// Log error for debugging
Log::error('File upload failed: ' . $e->getMessage(), [
'field' => $field,
'file' => $file->getClientOriginalName(),
'transaction_id' => $transaction->id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return back()->withErrors(['error' => 'Gagal mengupload file: ' . $field . '. Error: ' . $e->getMessage()]);
}
$storedPath = $this->processImageUpload($request, $field, $transaction);
if ($storedPath) {
$data[$field] = $storedPath;
}
}
@@ -144,6 +83,91 @@ class PostchecksController extends Controller
}
}
public function edit(Transaction $transaction, Postcheck $postcheck)
{
$acConditions = Postcheck::getAcConditionOptions();
$blowerConditions = Postcheck::getBlowerConditionOptions();
$evaporatorConditions = Postcheck::getEvaporatorConditionOptions();
$compressorConditions = Postcheck::getCompressorConditionOptions();
return view('transaction.postchecks.edit', compact(
'transaction',
'postcheck',
'acConditions',
'blowerConditions',
'evaporatorConditions',
'compressorConditions'
));
}
public function update(Request $request, Transaction $transaction, Postcheck $postcheck)
{
$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|image|mimes:jpeg,png,jpg|max:20480',
'ac_condition' => 'nullable|in:' . implode(',', Postcheck::getAcConditionOptions()),
'ac_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
'blower_condition' => 'nullable|in:' . implode(',', Postcheck::getBlowerConditionOptions()),
'blower_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
'evaporator_condition' => 'nullable|in:' . implode(',', Postcheck::getEvaporatorConditionOptions()),
'evaporator_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
'compressor_condition' => 'nullable|in:' . implode(',', Postcheck::getCompressorConditionOptions()),
'postcheck_notes' => 'nullable|string',
'front_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
]);
$updateData = [
'kilometer' => $request->kilometer,
'pressure_high' => $request->pressure_high,
'pressure_low' => $request->pressure_low,
'cabin_temperature' => $request->cabin_temperature,
'ac_condition' => $request->ac_condition,
'blower_condition' => $request->blower_condition,
'evaporator_condition' => $request->evaporator_condition,
'compressor_condition' => $request->compressor_condition,
'postcheck_notes' => $request->postcheck_notes,
];
$imageFields = [
'front_image', 'cabin_temperature_image', 'ac_image',
'blower_image', 'evaporator_image'
];
foreach ($imageFields as $field) {
$newPath = $this->processImageUpload($request, $field, $transaction);
if ($newPath) {
// delete old file if exists
if ($postcheck->{$field}) {
$this->deleteIfExists($postcheck->{$field});
}
$updateData[$field] = $newPath;
}
}
try {
$postcheck->update($updateData);
return redirect()->route('transaction')->with('success', 'Postcheck berhasil diperbarui');
} catch (\Exception $e) {
Log::error('Postcheck update failed: ' . $e->getMessage());
return back()->withErrors(['error' => 'Gagal memperbarui data postcheck. Silakan coba lagi.']);
}
}
public function print($transaction_id)
{
try {
$postcheck = Postcheck::where('transaction_id', $transaction_id)->firstOrFail();
return view('transaction.postchecks.print', compact('postcheck'));
} catch (\Exception $e) {
Log::error('Error printing postcheck: ' . $e->getMessage());
return back()->with('error', 'Gagal membuka halaman print postcheck.');
}
}
/**
* Ensure the base storage directory exists
*/
@@ -185,4 +209,69 @@ class PostchecksController extends Controller
rmdir($testDir);
Log::info('Storage directory is properly configured: ' . $storagePath);
}
/**
* Securely process image upload to prevent RCE.
* - Only allows jpeg and png
* - Generates safe filename
* - Validates actual image content using getimagesize
*/
private function processImageUpload(Request $request, string $field, Transaction $transaction): ?string
{
if (!($request->hasFile($field) && $request->file($field)->isValid())) {
return null;
}
$file = $request->file($field);
// Double-check mime type from PHP, disallow svg/gif
$allowedMimes = ['image/jpeg' => 'jpg', 'image/png' => 'png'];
$mime = $file->getMimeType();
if (!array_key_exists($mime, $allowedMimes)) {
throw new \RuntimeException('Tipe file tidak diperbolehkan');
}
// Verify it's a real image by reading dimensions
$imageInfo = @getimagesize($file->getRealPath());
if ($imageInfo === false) {
throw new \RuntimeException('File bukan gambar yang valid');
}
// Prepare directory
$directory = 'transactions/' . $transaction->id . '/postcheck';
$this->ensureStorageDirectoryExists();
if (!Storage::disk('public')->exists('transactions')) {
Storage::disk('public')->makeDirectory('transactions', 0755, true);
}
if (!Storage::disk('public')->exists('transactions/' . $transaction->id)) {
Storage::disk('public')->makeDirectory('transactions/' . $transaction->id, 0755, true);
}
if (!Storage::disk('public')->exists($directory)) {
Storage::disk('public')->makeDirectory($directory, 0755, true);
}
// Safe filename
$ext = $allowedMimes[$mime];
$filename = time() . '_' . bin2hex(random_bytes(6)) . '_' . $transaction->id . '_' . $field . '.' . $ext;
// Store
$path = $file->storeAs($directory, $filename, 'public');
Log::info('Secure image stored', ['field' => $field, 'path' => $path]);
return $path;
}
/**
* Delete a file from public storage if it exists
*/
private function deleteIfExists(string $path): void
{
try {
if ($path && Storage::disk('public')->exists($path)) {
Storage::disk('public')->delete($path);
}
} catch (\Throwable $e) {
Log::warning('Failed to delete old image', ['path' => $path, 'error' => $e->getMessage()]);
}
}
}

View File

@@ -11,14 +11,14 @@ use Illuminate\Support\Facades\Log;
class PrechecksController extends Controller
{
public function index(Transaction $transaction)
public function create(Transaction $transaction)
{
$acConditions = Precheck::getAcConditionOptions();
$blowerConditions = Precheck::getBlowerConditionOptions();
$evaporatorConditions = Precheck::getEvaporatorConditionOptions();
$compressorConditions = Precheck::getCompressorConditionOptions();
return view('transaction.prechecks', compact(
return view('transaction.prechecks.create', compact(
'transaction',
'acConditions',
'blowerConditions',
@@ -74,6 +74,11 @@ class PrechecksController extends Controller
try {
$file = $request->file($field);
// Enhanced security validation
if (!$this->isValidImageFile($file)) {
return back()->withErrors(['error' => 'File tidak valid atau berbahaya: ' . $field]);
}
// Generate unique filename with transaction ID
$filename = time() . '_' . uniqid() . '_' . $transaction->id . '_' . $field . '.' . $file->getClientOriginalExtension();
@@ -144,6 +149,158 @@ class PrechecksController extends Controller
}
}
public function edit(Transaction $transaction, Precheck $precheck)
{
$acConditions = Precheck::getAcConditionOptions();
$blowerConditions = Precheck::getBlowerConditionOptions();
$evaporatorConditions = Precheck::getEvaporatorConditionOptions();
$compressorConditions = Precheck::getCompressorConditionOptions();
return view('transaction.prechecks.edit', compact(
'transaction',
'precheck',
'acConditions',
'blowerConditions',
'evaporatorConditions',
'compressorConditions'
));
}
public function update(Request $request, Transaction $transaction, Precheck $precheck)
{
$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|image|mimes:jpeg,png,jpg|max:20480',
'ac_condition' => 'nullable|in:' . implode(',', Precheck::getAcConditionOptions()),
'ac_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
'blower_condition' => 'nullable|in:' . implode(',', Precheck::getBlowerConditionOptions()),
'blower_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
'evaporator_condition' => 'nullable|in:' . implode(',', Precheck::getEvaporatorConditionOptions()),
'evaporator_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
'compressor_condition' => 'nullable|in:' . implode(',', Precheck::getCompressorConditionOptions()),
'precheck_notes' => 'nullable|string',
'front_image' => 'nullable|image|mimes:jpeg,png,jpg|max:20480',
]);
$data = [
'kilometer' => $request->kilometer,
'pressure_high' => $request->pressure_high,
'pressure_low' => $request->pressure_low,
'cabin_temperature' => $request->cabin_temperature,
'ac_condition' => $request->ac_condition,
'blower_condition' => $request->blower_condition,
'evaporator_condition' => $request->evaporator_condition,
'compressor_condition' => $request->compressor_condition,
'precheck_notes' => $request->precheck_notes,
];
// Handle file uploads with security validation
$imageFields = [
'front_image', 'cabin_temperature_image', 'ac_image',
'blower_image', 'evaporator_image'
];
foreach ($imageFields as $field) {
if ($request->hasFile($field) && $request->file($field)->isValid()) {
try {
$file = $request->file($field);
// Enhanced security validation
if (!$this->isValidImageFile($file)) {
return back()->withErrors(['error' => 'File tidak valid atau berbahaya: ' . $field]);
}
// Generate unique filename with transaction ID
$filename = time() . '_' . uniqid() . '_' . $transaction->id . '_' . $field . '.' . $file->getClientOriginalExtension();
// Create directory path: transactions/{transaction_id}/precheck/
$directory = 'transactions/' . $transaction->id . '/precheck';
// Ensure base storage directory exists
$this->ensureStorageDirectoryExists();
// Ensure transactions directory exists
if (!Storage::disk('public')->exists('transactions')) {
Storage::disk('public')->makeDirectory('transactions', 0755, true);
Log::info('Created transactions directory');
}
// Ensure transaction ID directory exists
$transactionDir = 'transactions/' . $transaction->id;
if (!Storage::disk('public')->exists($transactionDir)) {
Storage::disk('public')->makeDirectory($transactionDir, 0755, true);
Log::info('Created transaction directory: ' . $transactionDir);
}
// Ensure precheck directory exists
if (!Storage::disk('public')->exists($directory)) {
Storage::disk('public')->makeDirectory($directory, 0755, true);
Log::info('Created precheck directory: ' . $directory);
}
// Delete old file if exists
if ($precheck->$field && Storage::disk('public')->exists($precheck->$field)) {
Storage::disk('public')->delete($precheck->$field);
}
// Store file in organized directory structure
$path = $file->storeAs($directory, $filename, 'public');
// Store file path
$data[$field] = $path;
// Store metadata
$data[$field . '_metadata'] = [
'original_name' => $file->getClientOriginalName(),
'size' => $file->getSize(),
'mime_type' => $file->getMimeType(),
'uploaded_at' => now()->toISOString(),
'transaction_id' => $transaction->id,
'filename' => $filename,
];
Log::info('File uploaded successfully: ' . $path);
} catch (\Exception $e) {
// Log error for debugging
Log::error('File upload failed: ' . $e->getMessage(), [
'field' => $field,
'file' => $file->getClientOriginalName(),
'transaction_id' => $transaction->id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return back()->withErrors(['error' => 'Gagal mengupload file: ' . $field . '. Error: ' . $e->getMessage()]);
}
}
}
try {
$precheck->update($data);
return redirect()->route('transaction')->with('success', 'Precheck berhasil diperbarui');
} catch (\Exception $e) {
Log::error('Precheck update failed: ' . $e->getMessage());
return back()->withErrors(['error' => 'Gagal memperbarui data precheck. Silakan coba lagi.']);
}
}
public function print($transaction_id)
{
try {
$precheck = Precheck::where('transaction_id', $transaction_id)->firstOrFail();
return view('transaction.prechecks.print', compact('precheck'));
} catch (\Exception $e) {
Log::error('Error printing precheck: ' . $e->getMessage());
return back()->with('error', 'Gagal membuka halaman print precheck.');
}
}
/**
* Ensure the base storage directory exists
*/
@@ -185,4 +342,138 @@ class PrechecksController extends Controller
rmdir($testDir);
Log::info('Storage directory is properly configured: ' . $storagePath);
}
/**
* Enhanced security validation for image files to prevent RCE attacks
*
* @param \Illuminate\Http\UploadedFile $file
* @return bool
*/
private function isValidImageFile($file)
{
try {
// 1. Check file extension (whitelist approach)
$allowedExtensions = ['jpg', 'jpeg', 'png'];
$extension = strtolower($file->getClientOriginalExtension());
if (!in_array($extension, $allowedExtensions)) {
Log::warning('Invalid file extension: ' . $extension, [
'filename' => $file->getClientOriginalName(),
'user_id' => auth()->id()
]);
return false;
}
// 2. Check MIME type
$allowedMimeTypes = [
'image/jpeg',
'image/jpg',
'image/png'
];
$mimeType = $file->getMimeType();
if (!in_array($mimeType, $allowedMimeTypes)) {
Log::warning('Invalid MIME type: ' . $mimeType, [
'filename' => $file->getClientOriginalName(),
'user_id' => auth()->id()
]);
return false;
}
// 3. Verify file is actually an image using getimagesize
$imageInfo = @getimagesize($file->getPathname());
if ($imageInfo === false) {
Log::warning('File is not a valid image: ' . $file->getClientOriginalName(), [
'user_id' => auth()->id()
]);
return false;
}
// 4. Check image dimensions (prevent extremely large images)
$maxWidth = 5000;
$maxHeight = 5000;
if ($imageInfo[0] > $maxWidth || $imageInfo[1] > $maxHeight) {
Log::warning('Image dimensions too large: ' . $imageInfo[0] . 'x' . $imageInfo[1], [
'filename' => $file->getClientOriginalName(),
'user_id' => auth()->id()
]);
return false;
}
// 5. Check file size (max 20MB)
$maxSize = 20 * 1024 * 1024; // 20MB
if ($file->getSize() > $maxSize) {
Log::warning('File size too large: ' . $file->getSize(), [
'filename' => $file->getClientOriginalName(),
'user_id' => auth()->id()
]);
return false;
}
// 6. Check for suspicious content in filename
$filename = $file->getClientOriginalName();
$suspiciousPatterns = [
'<?php', '<?=', '<script', 'javascript:', 'data:', 'vbscript:',
'..', '~', '$', '`', '|', '&', ';', '(', ')', '{', '}',
'exec', 'system', 'shell_exec', 'passthru', 'eval'
];
foreach ($suspiciousPatterns as $pattern) {
if (stripos($filename, $pattern) !== false) {
Log::warning('Suspicious filename pattern detected: ' . $pattern, [
'filename' => $filename,
'user_id' => auth()->id()
]);
return false;
}
}
// 7. Additional security: Check file header (magic bytes)
$handle = fopen($file->getPathname(), 'rb');
if ($handle) {
$header = fread($handle, 8);
fclose($handle);
// Check for valid image headers
$validHeaders = [
"\xFF\xD8\xFF", // JPEG
"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A", // PNG
];
$isValidHeader = false;
foreach ($validHeaders as $validHeader) {
if (substr($header, 0, strlen($validHeader)) === $validHeader) {
$isValidHeader = true;
break;
}
}
if (!$isValidHeader) {
Log::warning('Invalid file header detected', [
'filename' => $filename,
'user_id' => auth()->id(),
'header' => bin2hex($header)
]);
return false;
}
}
Log::info('File validation passed', [
'filename' => $filename,
'size' => $file->getSize(),
'mime_type' => $mimeType,
'user_id' => auth()->id()
]);
return true;
} catch (\Exception $e) {
Log::error('File validation error: ' . $e->getMessage(), [
'filename' => $file->getClientOriginalName(),
'user_id' => auth()->id(),
'error' => $e->getMessage()
]);
return false;
}
}
}

View File

@@ -20,7 +20,6 @@ class adminRole
{
// check if user can access admin area
$user = Privilege::join('menus AS m', 'm.id', '=', 'privileges.menu_id')->where('m.link', 'adminarea')->where('role_id', Auth::user()->role_id)->where('view', 1)->get();
// dd($user);
if (!$user) {
abort(404);
}

View File

@@ -75,7 +75,7 @@ class Postcheck extends Model
*/
public function getFrontImageUrlAttribute()
{
return $this->front_image ? Storage::disk('public')->url($this->front_image) : null;
return $this->front_image ? asset('storage/' . ltrim($this->front_image, '/')) : null;
}
/**
@@ -83,7 +83,7 @@ class Postcheck extends Model
*/
public function getCabinTemperatureImageUrlAttribute()
{
return $this->cabin_temperature_image ? Storage::disk('public')->url($this->cabin_temperature_image) : null;
return $this->cabin_temperature_image ? asset('storage/' . ltrim($this->cabin_temperature_image, '/')) : null;
}
/**
@@ -91,7 +91,7 @@ class Postcheck extends Model
*/
public function getAcImageUrlAttribute()
{
return $this->ac_image ? Storage::disk('public')->url($this->ac_image) : null;
return $this->ac_image ? asset('storage/' . ltrim($this->ac_image, '/')) : null;
}
/**
@@ -99,7 +99,7 @@ class Postcheck extends Model
*/
public function getBlowerImageUrlAttribute()
{
return $this->blower_image ? Storage::disk('public')->url($this->blower_image) : null;
return $this->blower_image ? asset('storage/' . ltrim($this->blower_image, '/')) : null;
}
/**
@@ -107,7 +107,7 @@ class Postcheck extends Model
*/
public function getEvaporatorImageUrlAttribute()
{
return $this->evaporator_image ? Storage::disk('public')->url($this->evaporator_image) : null;
return $this->evaporator_image ? asset('storage/' . ltrim($this->evaporator_image, '/')) : null;
}
/**

View File

@@ -75,7 +75,7 @@ class Precheck extends Model
*/
public function getFrontImageUrlAttribute()
{
return $this->front_image ? Storage::disk('public')->url($this->front_image) : null;
return $this->front_image ? asset('storage/' . ltrim($this->front_image, '/')) : null;
}
/**
@@ -83,7 +83,7 @@ class Precheck extends Model
*/
public function getCabinTemperatureImageUrlAttribute()
{
return $this->cabin_temperature_image ? Storage::disk('public')->url($this->cabin_temperature_image) : null;
return $this->cabin_temperature_image ? asset('storage/' . ltrim($this->cabin_temperature_image, '/')) : null;
}
/**
@@ -91,7 +91,7 @@ class Precheck extends Model
*/
public function getAcImageUrlAttribute()
{
return $this->ac_image ? Storage::disk('public')->url($this->ac_image) : null;
return $this->ac_image ? asset('storage/' . ltrim($this->ac_image, '/')) : null;
}
/**
@@ -99,7 +99,7 @@ class Precheck extends Model
*/
public function getBlowerImageUrlAttribute()
{
return $this->blower_image ? Storage::disk('public')->url($this->blower_image) : null;
return $this->blower_image ? asset('storage/' . ltrim($this->blower_image, '/')) : null;
}
/**
@@ -107,7 +107,7 @@ class Precheck extends Model
*/
public function getEvaporatorImageUrlAttribute()
{
return $this->evaporator_image ? Storage::disk('public')->url($this->evaporator_image) : null;
return $this->evaporator_image ? asset('storage/' . ltrim($this->evaporator_image, '/')) : null;
}
/**