create feature sa create list claim and price to work per dealer
This commit is contained in:
@@ -52,10 +52,10 @@ class TransactionController extends Controller
|
||||
// Get KPI data for current user using KPI service
|
||||
$kpiService = app(\App\Services\KpiService::class);
|
||||
|
||||
// Auto-calculate current month KPI achievement to ensure data is up-to-date
|
||||
$kpiService->calculateKpiAchievement(Auth::user());
|
||||
// Auto-calculate current month KPI achievement including claimed transactions
|
||||
$kpiService->calculateKpiAchievementWithClaims(Auth::user());
|
||||
|
||||
$kpiSummary = $kpiService->getKpiSummary(Auth::user());
|
||||
$kpiSummary = $kpiService->getKpiSummaryWithClaims(Auth::user());
|
||||
|
||||
// Get current month period name
|
||||
$currentMonthName = now()->translatedFormat('F Y');
|
||||
@@ -896,7 +896,7 @@ class TransactionController extends Controller
|
||||
"warranty" => $request->warranty,
|
||||
"user_sa_id" => $request->user_sa_id,
|
||||
"date" => $request->date,
|
||||
"status" => 'completed', // Mark as completed to trigger stock reduction
|
||||
"status" => 0, // pending (0) - Mark as pending initially
|
||||
"created_at" => date('Y-m-d H:i:s'),
|
||||
"updated_at" => date('Y-m-d H:i:s')
|
||||
];
|
||||
@@ -919,6 +919,10 @@ class TransactionController extends Controller
|
||||
$this->stockService->reduceStockForTransaction($transaction);
|
||||
}
|
||||
|
||||
// Recalculate KPI achievement after creating transactions
|
||||
$kpiService = app(\App\Services\KpiService::class);
|
||||
$kpiService->calculateKpiAchievementWithClaims(Auth::user());
|
||||
|
||||
DB::commit();
|
||||
return redirect()->back()->with('success', 'Berhasil input pekerjaan dan stock telah dikurangi otomatis');
|
||||
|
||||
@@ -1018,4 +1022,256 @@ class TransactionController extends Controller
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get claim transactions for DataTable - Only for mechanics
|
||||
*/
|
||||
public function getClaimTransactions(Request $request)
|
||||
{
|
||||
// Only allow mechanics to access this endpoint
|
||||
if (Auth::user()->role_id != 3) {
|
||||
return response()->json([
|
||||
'draw' => intval($request->input('draw')),
|
||||
'recordsTotal' => 0,
|
||||
'recordsFiltered' => 0,
|
||||
'data' => []
|
||||
]);
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'dealer_id' => 'required|exists:dealers,id'
|
||||
]);
|
||||
|
||||
try {
|
||||
$query = 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')
|
||||
->select([
|
||||
'transactions.id',
|
||||
'transactions.date',
|
||||
'transactions.spk',
|
||||
'transactions.police_number',
|
||||
'transactions.qty',
|
||||
'transactions.status',
|
||||
'transactions.claimed_at',
|
||||
'transactions.claimed_by',
|
||||
'w.name as work_name',
|
||||
'sa.name as sa_name',
|
||||
'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');
|
||||
|
||||
// Handle DataTables server-side processing
|
||||
$total = $query->count();
|
||||
|
||||
// Search functionality
|
||||
if ($request->has('search') && !empty($request->search['value'])) {
|
||||
$searchValue = $request->search['value'];
|
||||
$query->where(function($q) use ($searchValue) {
|
||||
$q->where('transactions.spk', 'like', "%{$searchValue}%")
|
||||
->orWhere('transactions.police_number', 'like', "%{$searchValue}%")
|
||||
->orWhere('w.name', 'like', "%{$searchValue}%")
|
||||
->orWhere('sa.name', 'like', "%{$searchValue}%")
|
||||
->orWhere('users.name', 'like', "%{$searchValue}%");
|
||||
});
|
||||
}
|
||||
|
||||
$filteredTotal = $query->count();
|
||||
|
||||
// Pagination
|
||||
$start = $request->input('start', 0);
|
||||
$length = $request->input('length', 15);
|
||||
$query->skip($start)->take($length);
|
||||
|
||||
$transactions = $query->get();
|
||||
|
||||
$data = [];
|
||||
foreach ($transactions as $transaction) {
|
||||
$data[] = [
|
||||
'date' => date('d/m/Y', strtotime($transaction->date)),
|
||||
'spk' => $transaction->spk,
|
||||
'police_number' => $transaction->police_number,
|
||||
'work_name' => $transaction->work_name,
|
||||
'qty' => number_format($transaction->qty),
|
||||
'sa_name' => $transaction->sa_name,
|
||||
'status' => $this->getStatusBadge($transaction->status),
|
||||
'action' => $this->getActionButtons($transaction),
|
||||
'claimed_at' => $transaction->claimed_at,
|
||||
'claimed_by' => $transaction->claimed_by
|
||||
];
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'draw' => intval($request->input('draw')),
|
||||
'recordsTotal' => $total,
|
||||
'recordsFiltered' => $filteredTotal,
|
||||
'data' => $data
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
return response()->json([
|
||||
'error' => 'Error fetching claim transactions: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status badge HTML
|
||||
*/
|
||||
private function getStatusBadge($status)
|
||||
{
|
||||
switch ($status) {
|
||||
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>';
|
||||
default:
|
||||
return '<span class="badge badge-secondary">Tidak Diketahui</span>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Claim a transaction - Only for mechanics
|
||||
*/
|
||||
public function claim($id)
|
||||
{
|
||||
// Only allow mechanics to claim transactions
|
||||
if (Auth::user()->role_id != 3) {
|
||||
return response()->json([
|
||||
'status' => 403,
|
||||
'message' => 'Hanya mekanik yang dapat mengklaim pekerjaan'
|
||||
], 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$transaction = Transaction::find($id);
|
||||
|
||||
if (!$transaction) {
|
||||
return response()->json([
|
||||
'status' => 404,
|
||||
'message' => 'Transaksi tidak ditemukan'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Check if transaction belongs to current user's dealer
|
||||
if ($transaction->dealer_id !== Auth::user()->dealer_id) {
|
||||
return response()->json([
|
||||
'status' => 403,
|
||||
'message' => 'Anda tidak memiliki akses ke transaksi ini'
|
||||
], 403);
|
||||
}
|
||||
|
||||
// Check if transaction can be claimed (pending or completed)
|
||||
if (!in_array($transaction->status, [0, 1])) { // pending (0) and completed (1)
|
||||
return response()->json([
|
||||
'status' => 400,
|
||||
'message' => 'Hanya transaksi yang menunggu atau sudah selesai yang dapat diklaim'
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Check if transaction is already claimed
|
||||
if (!empty($transaction->claimed_at) || !empty($transaction->claimed_by)) {
|
||||
return response()->json([
|
||||
'status' => 400,
|
||||
'message' => 'Transaksi ini sudah diklaim sebelumnya'
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Check if transaction was created by SA (role_id = 4)
|
||||
$creator = User::find($transaction->user_id);
|
||||
if (!$creator || $creator->role_id != 4) {
|
||||
return response()->json([
|
||||
'status' => 400,
|
||||
'message' => 'Hanya transaksi yang dibuat oleh Service Advisor yang dapat diklaim'
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Update transaction with claim information
|
||||
$transaction->update([
|
||||
'claimed_at' => now(),
|
||||
'claimed_by' => Auth::user()->id
|
||||
]);
|
||||
|
||||
// Recalculate KPI achievement after claiming
|
||||
$kpiService = app(\App\Services\KpiService::class);
|
||||
$kpiService->calculateKpiAchievementWithClaims(Auth::user());
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'message' => 'Pekerjaan berhasil diklaim'
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 500,
|
||||
'message' => 'Gagal mengklaim pekerjaan: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get action buttons HTML for claim transactions - Only for mechanics
|
||||
*/
|
||||
private function getActionButtons($transaction)
|
||||
{
|
||||
$buttons = '';
|
||||
|
||||
// Only show 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>';
|
||||
} else {
|
||||
$buttons .= '<span class="badge badge-info">Sudah Diklaim</span>';
|
||||
}
|
||||
}
|
||||
|
||||
return $buttons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get KPI data for AJAX refresh
|
||||
*/
|
||||
public function getKpiData()
|
||||
{
|
||||
try {
|
||||
$kpiService = app(\App\Services\KpiService::class);
|
||||
$kpiSummary = $kpiService->getKpiSummaryWithClaims(Auth::user());
|
||||
|
||||
$currentMonthName = now()->translatedFormat('F Y');
|
||||
|
||||
$kpiData = [
|
||||
'target' => $kpiSummary['current_target'] ? $kpiSummary['current_target']->target_value : 0,
|
||||
'actual' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->actual_value : 0,
|
||||
'percentage' => $kpiSummary['current_percentage'],
|
||||
'status' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->status : 'pending',
|
||||
'status_color' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->status_color : 'secondary',
|
||||
'period' => $currentMonthName,
|
||||
'has_target' => $kpiSummary['current_target'] ? true : false
|
||||
];
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $kpiData
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Error fetching KPI data: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,13 @@ class WorkController extends Controller
|
||||
</a>';
|
||||
}
|
||||
|
||||
// Set Prices Button
|
||||
if(Gate::allows('view', $menu)) {
|
||||
$btn .= '<a href="'. route('work.set-prices', ['work' => $row->work_id]) .'" class="btn btn-primary btn-sm" title="Set Harga per Dealer">
|
||||
Harga
|
||||
</a>';
|
||||
}
|
||||
|
||||
if(Gate::allows('update', $menu)) {
|
||||
$btn .= '<button class="btn btn-warning btn-sm" id="editWork'. $row->work_id .'" data-url="'. route('work.edit', $row->work_id) .'" data-action="'. route('work.update', $row->work_id) .'" onclick="editWork('. $row->work_id .')">
|
||||
Edit
|
||||
@@ -157,4 +164,20 @@ class WorkController extends Controller
|
||||
|
||||
return response()->json($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for setting prices per dealer for a specific work.
|
||||
*
|
||||
* @param \App\Models\Work $work
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function showPrices(Work $work)
|
||||
{
|
||||
$menu = Menu::where('link', 'work.index')->first();
|
||||
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
|
||||
|
||||
$dealers = \App\Models\Dealer::all();
|
||||
|
||||
return view('back.master.work_prices', compact('work', 'dealers'));
|
||||
}
|
||||
}
|
||||
|
||||
363
app/Http/Controllers/WorkDealerPriceController.php
Normal file
363
app/Http/Controllers/WorkDealerPriceController.php
Normal file
@@ -0,0 +1,363 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Work;
|
||||
use App\Models\Dealer;
|
||||
use App\Models\WorkDealerPrice;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Yajra\DataTables\DataTables;
|
||||
|
||||
class WorkDealerPriceController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of work prices for a specific work
|
||||
*/
|
||||
public function index(Request $request, Work $work)
|
||||
{
|
||||
if ($request->ajax()) {
|
||||
$data = WorkDealerPrice::with(['dealer'])
|
||||
->where('work_id', $work->id)
|
||||
->select('work_dealer_prices.*');
|
||||
|
||||
return DataTables::of($data)
|
||||
->addIndexColumn()
|
||||
->addColumn('dealer_name', function($row) {
|
||||
return $row->dealer->name;
|
||||
})
|
||||
->addColumn('formatted_price', function($row) {
|
||||
return $row->formatted_price;
|
||||
})
|
||||
->addColumn('action', function($row) {
|
||||
$btn = '<div class="d-flex flex-row gap-1">';
|
||||
$btn .= '<button class="btn btn-warning btn-sm" onclick="editPrice(' . $row->id . ')" title="Edit Harga">
|
||||
<i class="fa fa-edit"></i>
|
||||
</button>';
|
||||
$btn .= '<button class="btn btn-danger btn-sm" onclick="deletePrice(' . $row->id . ')" title="Hapus Harga">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>';
|
||||
$btn .= '</div>';
|
||||
return $btn;
|
||||
})
|
||||
->rawColumns(['action'])
|
||||
->make(true);
|
||||
}
|
||||
|
||||
$dealers = Dealer::all();
|
||||
return view('back.master.work_prices', compact('work', 'dealers'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created price
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'work_id' => 'required|exists:works,id',
|
||||
'dealer_id' => 'required|exists:dealers,id',
|
||||
'price' => 'required|numeric|min:0',
|
||||
'currency' => 'required|string|max:3',
|
||||
'is_active' => 'nullable|in:0,1',
|
||||
], [
|
||||
'work_id.required' => 'ID pekerjaan harus diisi',
|
||||
'work_id.exists' => 'Pekerjaan tidak ditemukan',
|
||||
'dealer_id.required' => 'ID dealer harus diisi',
|
||||
'dealer_id.exists' => 'Dealer tidak ditemukan',
|
||||
'price.required' => 'Harga harus diisi',
|
||||
'price.numeric' => 'Harga harus berupa angka',
|
||||
'price.min' => 'Harga minimal 0',
|
||||
'currency.required' => 'Mata uang harus diisi',
|
||||
'currency.max' => 'Mata uang maksimal 3 karakter',
|
||||
'is_active.in' => 'Status aktif harus 0 atau 1',
|
||||
]);
|
||||
|
||||
// Check if price already exists for this work-dealer combination (including soft deleted)
|
||||
$existingPrice = WorkDealerPrice::withTrashed()
|
||||
->where('work_id', $request->work_id)
|
||||
->where('dealer_id', $request->dealer_id)
|
||||
->first();
|
||||
|
||||
// Also check for active records to prevent duplicates
|
||||
$activePrice = WorkDealerPrice::where('work_id', $request->work_id)
|
||||
->where('dealer_id', $request->dealer_id)
|
||||
->where('id', '!=', $existingPrice ? $existingPrice->id : 0)
|
||||
->first();
|
||||
|
||||
if ($activePrice) {
|
||||
return response()->json([
|
||||
'status' => 422,
|
||||
'message' => 'Harga untuk dealer ini sudah ada. Silakan edit harga yang sudah ada.'
|
||||
], 422);
|
||||
}
|
||||
|
||||
// Use database transaction to prevent race conditions
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
|
||||
if ($existingPrice) {
|
||||
if ($existingPrice->trashed()) {
|
||||
// Restore soft deleted record and update
|
||||
$existingPrice->restore();
|
||||
}
|
||||
|
||||
// Update existing price
|
||||
$existingPrice->update([
|
||||
'price' => $request->price,
|
||||
'currency' => $request->currency,
|
||||
'is_active' => $request->has('is_active') ? (bool)$request->is_active : true,
|
||||
]);
|
||||
$price = $existingPrice;
|
||||
|
||||
$message = 'Harga berhasil diperbarui';
|
||||
} else {
|
||||
// Create new price
|
||||
$price = WorkDealerPrice::create([
|
||||
'work_id' => $request->work_id,
|
||||
'dealer_id' => $request->dealer_id,
|
||||
'price' => $request->price,
|
||||
'currency' => $request->currency,
|
||||
'is_active' => $request->has('is_active') ? (bool)$request->is_active : true,
|
||||
]);
|
||||
|
||||
$message = 'Harga berhasil disimpan';
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'data' => $price,
|
||||
'message' => $message
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollback();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
return response()->json([
|
||||
'status' => 422,
|
||||
'message' => 'Validasi gagal',
|
||||
'errors' => $e->errors()
|
||||
], 422);
|
||||
} catch (\Illuminate\Database\QueryException $e) {
|
||||
// Handle unique constraint violation
|
||||
if ($e->getCode() == 23000) {
|
||||
return response()->json([
|
||||
'status' => 422,
|
||||
'message' => 'Harga untuk dealer ini sudah ada. Silakan edit harga yang sudah ada.'
|
||||
], 422);
|
||||
}
|
||||
return response()->json([
|
||||
'status' => 500,
|
||||
'message' => 'Terjadi kesalahan database: ' . $e->getMessage()
|
||||
], 500);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 500,
|
||||
'message' => 'Terjadi kesalahan: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified price
|
||||
*/
|
||||
public function edit($id)
|
||||
{
|
||||
$price = WorkDealerPrice::with(['work', 'dealer'])->findOrFail($id);
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'data' => $price,
|
||||
'message' => 'Data harga berhasil diambil'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified price
|
||||
*/
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$request->validate([
|
||||
'price' => 'required|numeric|min:0',
|
||||
'currency' => 'required|string|max:3',
|
||||
'is_active' => 'boolean',
|
||||
]);
|
||||
|
||||
$price = WorkDealerPrice::findOrFail($id);
|
||||
$price->update($request->all());
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'message' => 'Harga berhasil diperbarui'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified price
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
try {
|
||||
$price = WorkDealerPrice::findOrFail($id);
|
||||
$price->delete(); // Soft delete
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'message' => 'Harga berhasil dihapus'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 500,
|
||||
'message' => 'Terjadi kesalahan saat menghapus harga: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price for specific work and dealer
|
||||
*/
|
||||
public function getPrice(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'work_id' => 'required|exists:works,id',
|
||||
'dealer_id' => 'required|exists:dealers,id',
|
||||
]);
|
||||
|
||||
$price = WorkDealerPrice::getPriceForWorkAndDealer(
|
||||
$request->work_id,
|
||||
$request->dealer_id
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'data' => $price,
|
||||
'message' => $price ? 'Harga ditemukan' : 'Harga tidak ditemukan'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle status of a price
|
||||
*/
|
||||
public function toggleStatus(Request $request, Work $work)
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'dealer_id' => 'required|exists:dealers,id',
|
||||
'is_active' => 'required|in:0,1,true,false',
|
||||
], [
|
||||
'dealer_id.required' => 'ID dealer harus diisi',
|
||||
'dealer_id.exists' => 'Dealer tidak ditemukan',
|
||||
'is_active.required' => 'Status aktif harus diisi',
|
||||
'is_active.in' => 'Status aktif harus 0, 1, true, atau false',
|
||||
]);
|
||||
|
||||
// Convert string values to boolean
|
||||
$isActive = filter_var($request->is_active, FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
// Find existing price (including soft deleted)
|
||||
$existingPrice = WorkDealerPrice::withTrashed()
|
||||
->where('work_id', $work->id)
|
||||
->where('dealer_id', $request->dealer_id)
|
||||
->first();
|
||||
|
||||
if (!$existingPrice) {
|
||||
// Create new record with default price 0 if no record exists
|
||||
$existingPrice = WorkDealerPrice::create([
|
||||
'work_id' => $work->id,
|
||||
'dealer_id' => $request->dealer_id,
|
||||
'price' => 0,
|
||||
'currency' => 'IDR',
|
||||
'is_active' => $isActive,
|
||||
]);
|
||||
} else {
|
||||
// Restore if soft deleted
|
||||
if ($existingPrice->trashed()) {
|
||||
$existingPrice->restore();
|
||||
}
|
||||
|
||||
// Update status
|
||||
$existingPrice->update([
|
||||
'is_active' => $isActive
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'data' => $existingPrice,
|
||||
'message' => 'Status berhasil diubah menjadi ' . ($isActive ? 'Aktif' : 'Nonaktif')
|
||||
]);
|
||||
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
return response()->json([
|
||||
'status' => 422,
|
||||
'message' => 'Validasi gagal',
|
||||
'errors' => $e->errors()
|
||||
], 422);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 500,
|
||||
'message' => 'Terjadi kesalahan: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk create prices for a work
|
||||
*/
|
||||
public function bulkCreate(Request $request, Work $work)
|
||||
{
|
||||
$request->validate([
|
||||
'prices' => 'required|array',
|
||||
'prices.*.dealer_id' => 'required|exists:dealers,id',
|
||||
'prices.*.price' => 'required|numeric|min:0',
|
||||
'prices.*.currency' => 'required|string|max:3',
|
||||
]);
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
foreach ($request->prices as $priceData) {
|
||||
// Check if price already exists
|
||||
$existingPrice = WorkDealerPrice::where('work_id', $work->id)
|
||||
->where('dealer_id', $priceData['dealer_id'])
|
||||
->first();
|
||||
|
||||
if ($existingPrice) {
|
||||
// Update existing price
|
||||
$existingPrice->update([
|
||||
'price' => $priceData['price'],
|
||||
'currency' => $priceData['currency'],
|
||||
'is_active' => true,
|
||||
]);
|
||||
} else {
|
||||
// Create new price
|
||||
WorkDealerPrice::create([
|
||||
'work_id' => $work->id,
|
||||
'dealer_id' => $priceData['dealer_id'],
|
||||
'price' => $priceData['price'],
|
||||
'currency' => $priceData['currency'],
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'message' => 'Harga berhasil disimpan'
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollback();
|
||||
return response()->json([
|
||||
'status' => 500,
|
||||
'message' => 'Terjadi kesalahan: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,4 +48,38 @@ class Dealer extends Model
|
||||
->withPivot('quantity')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all work prices for this dealer
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function workPrices()
|
||||
{
|
||||
return $this->hasMany(WorkDealerPrice::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price for specific work
|
||||
*
|
||||
* @param int $workId
|
||||
* @return WorkDealerPrice|null
|
||||
*/
|
||||
public function getPriceForWork($workId)
|
||||
{
|
||||
return $this->workPrices()
|
||||
->where('work_id', $workId)
|
||||
->active()
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active work prices for this dealer
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function activeWorkPrices()
|
||||
{
|
||||
return $this->hasMany(WorkDealerPrice::class)->active();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,12 @@ class Transaction extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
protected $fillable = [
|
||||
"user_id", "user_sa_id", "work_id", "form", "spk", "police_number", "warranty", "date", "qty", "status", "dealer_id"
|
||||
"user_id", "user_sa_id", "work_id", "form", "spk", "police_number", "warranty", "date", "qty", "status", "dealer_id",
|
||||
"claimed_at", "claimed_by"
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'claimed_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -54,4 +54,52 @@ class Work extends Model
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all dealer prices for this work
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function dealerPrices()
|
||||
{
|
||||
return $this->hasMany(WorkDealerPrice::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price for specific dealer
|
||||
*
|
||||
* @param int $dealerId
|
||||
* @return WorkDealerPrice|null
|
||||
*/
|
||||
public function getPriceForDealer($dealerId)
|
||||
{
|
||||
return $this->dealerPrices()
|
||||
->where('dealer_id', $dealerId)
|
||||
->active()
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price for specific dealer (including soft deleted)
|
||||
*
|
||||
* @param int $dealerId
|
||||
* @return WorkDealerPrice|null
|
||||
*/
|
||||
public function getPriceForDealerWithTrashed($dealerId)
|
||||
{
|
||||
return $this->dealerPrices()
|
||||
->withTrashed()
|
||||
->where('dealer_id', $dealerId)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active prices for this work
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function activeDealerPrices()
|
||||
{
|
||||
return $this->hasMany(WorkDealerPrice::class)->active();
|
||||
}
|
||||
}
|
||||
|
||||
81
app/Models/WorkDealerPrice.php
Normal file
81
app/Models/WorkDealerPrice.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class WorkDealerPrice extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'work_id',
|
||||
'dealer_id',
|
||||
'price',
|
||||
'currency',
|
||||
'is_active'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'price' => 'decimal:2',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the work associated with the price
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function work()
|
||||
{
|
||||
return $this->belongsTo(Work::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dealer associated with the price
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function dealer()
|
||||
{
|
||||
return $this->belongsTo(Dealer::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get only active prices
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted price with currency
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFormattedPriceAttribute()
|
||||
{
|
||||
return number_format($this->price, 0, ',', '.') . ' ' . $this->currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price for specific work and dealer
|
||||
*
|
||||
* @param int $workId
|
||||
* @param int $dealerId
|
||||
* @return WorkDealerPrice|null
|
||||
*/
|
||||
public static function getPriceForWorkAndDealer($workId, $dealerId)
|
||||
{
|
||||
return static::where('work_id', $workId)
|
||||
->where('dealer_id', $dealerId)
|
||||
->active()
|
||||
->first();
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ class KpiService
|
||||
private function getActualWorkCount(User $user, $year, $month)
|
||||
{
|
||||
return Transaction::where('user_id', $user->id)
|
||||
->where('status', 'completed')
|
||||
->whereIn('status', [0, 1]) // pending (0) and completed (1)
|
||||
->whereYear('date', $year)
|
||||
->whereMonth('date', $month)
|
||||
->sum('qty');
|
||||
@@ -329,4 +329,125 @@ class KpiService
|
||||
'is_on_track' => $currentAchievement ? $currentAchievement->achievement_percentage >= 100 : false
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get claimed transactions count for a mechanic
|
||||
*
|
||||
* @param User $user
|
||||
* @param int|null $year
|
||||
* @param int|null $month
|
||||
* @return int
|
||||
*/
|
||||
public function getClaimedTransactionsCount(User $user, $year = null, $month = null)
|
||||
{
|
||||
$year = $year ?? now()->year;
|
||||
$month = $month ?? now()->month;
|
||||
|
||||
return Transaction::where('claimed_by', $user->id)
|
||||
->whereNotNull('claimed_at')
|
||||
->whereYear('claimed_at', $year)
|
||||
->whereMonth('claimed_at', $month)
|
||||
->sum('qty');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate KPI achievement including claimed transactions
|
||||
*
|
||||
* @param User $user
|
||||
* @param int $year
|
||||
* @param int $month
|
||||
* @return KpiAchievement|null
|
||||
*/
|
||||
public function calculateKpiAchievementWithClaims(User $user, $year = null, $month = null)
|
||||
{
|
||||
$year = $year ?? now()->year;
|
||||
$month = $month ?? now()->month;
|
||||
|
||||
// Get current KPI target
|
||||
$kpiTarget = $user->kpiTargets()
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
if (!$kpiTarget) {
|
||||
Log::info("No KPI target found for user {$user->id}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate actual value including claimed transactions
|
||||
$actualValue = $this->getActualWorkCountWithClaims($user, $year, $month);
|
||||
|
||||
// Calculate percentage
|
||||
$achievementPercentage = $kpiTarget->target_value > 0
|
||||
? ($actualValue / $kpiTarget->target_value) * 100
|
||||
: 0;
|
||||
|
||||
// Save or update achievement
|
||||
return KpiAchievement::updateOrCreate(
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'year' => $year,
|
||||
'month' => $month
|
||||
],
|
||||
[
|
||||
'kpi_target_id' => $kpiTarget->id,
|
||||
'target_value' => $kpiTarget->target_value,
|
||||
'actual_value' => $actualValue,
|
||||
'achievement_percentage' => $achievementPercentage
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get actual work count including claimed transactions
|
||||
*
|
||||
* @param User $user
|
||||
* @param int $year
|
||||
* @param int $month
|
||||
* @return int
|
||||
*/
|
||||
private function getActualWorkCountWithClaims(User $user, $year, $month)
|
||||
{
|
||||
// Get transactions created by the user (including pending and completed)
|
||||
$createdTransactions = Transaction::where('user_id', $user->id)
|
||||
->whereIn('status', [0, 1]) // pending (0) and completed (1)
|
||||
->whereYear('date', $year)
|
||||
->whereMonth('date', $month)
|
||||
->sum('qty');
|
||||
|
||||
// Get transactions claimed by the user
|
||||
$claimedTransactions = Transaction::where('claimed_by', $user->id)
|
||||
->whereNotNull('claimed_at')
|
||||
->whereYear('claimed_at', $year)
|
||||
->whereMonth('claimed_at', $month)
|
||||
->sum('qty');
|
||||
|
||||
return $createdTransactions + $claimedTransactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get KPI summary including claimed transactions
|
||||
*
|
||||
* @param User $user
|
||||
* @return array
|
||||
*/
|
||||
public function getKpiSummaryWithClaims(User $user)
|
||||
{
|
||||
$currentYear = now()->year;
|
||||
$currentMonth = now()->month;
|
||||
|
||||
// Calculate current month achievement including claims
|
||||
$currentAchievement = $this->calculateKpiAchievementWithClaims($user, $currentYear, $currentMonth);
|
||||
|
||||
// Get current month target
|
||||
$currentTarget = $user->kpiTargets()
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
return [
|
||||
'current_achievement' => $currentAchievement,
|
||||
'current_target' => $currentTarget,
|
||||
'current_percentage' => $currentAchievement ? $currentAchievement->achievement_percentage : 0,
|
||||
'is_on_track' => $currentAchievement ? $currentAchievement->achievement_percentage >= 100 : false
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddClaimedColumnsToTransactionsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('transactions', function (Blueprint $table) {
|
||||
$table->timestamp('claimed_at')->nullable()->after('status');
|
||||
$table->unsignedBigInteger('claimed_by')->nullable()->after('claimed_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('transactions', function (Blueprint $table) {
|
||||
$table->dropColumn(['claimed_at', 'claimed_by']);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateWorkDealerPricesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('work_dealer_prices', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('work_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('dealer_id')->constrained()->onDelete('cascade');
|
||||
$table->decimal('price', 15, 2)->default(0.00);
|
||||
$table->string('currency', 3)->default('IDR');
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['work_id', 'dealer_id']);
|
||||
$table->index(['dealer_id', 'is_active']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('work_dealer_prices');
|
||||
}
|
||||
}
|
||||
655
public/js/pages/back/master/work-prices.js
Normal file
655
public/js/pages/back/master/work-prices.js
Normal file
@@ -0,0 +1,655 @@
|
||||
"use strict";
|
||||
|
||||
// Class definition
|
||||
var WorkPrices = (function () {
|
||||
// Private variables
|
||||
var workId;
|
||||
var dealersTable;
|
||||
var saveTimeout = {}; // For debouncing save requests
|
||||
|
||||
// Loading overlay functions
|
||||
var showLoadingOverlay = function (message) {
|
||||
if ($("#loading-overlay").length === 0) {
|
||||
$("body").append(`
|
||||
<div id="loading-overlay" style="
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
">
|
||||
<div class="spinner-border text-light" role="status" style="width: 3rem; height: 3rem;">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<div class="text-light mt-3" style="font-size: 1.1rem; font-weight: 500;">${message}</div>
|
||||
</div>
|
||||
`);
|
||||
} else {
|
||||
$("#loading-overlay").show();
|
||||
$("#loading-overlay .text-light:last").text(message);
|
||||
}
|
||||
};
|
||||
|
||||
var hideLoadingOverlay = function () {
|
||||
$("#loading-overlay").hide();
|
||||
};
|
||||
|
||||
// Private functions
|
||||
var initTable = function () {
|
||||
dealersTable = $("#dealers-table").DataTable({
|
||||
pageLength: -1, // Show all records
|
||||
lengthChange: false, // Hide length change dropdown
|
||||
searching: true,
|
||||
ordering: true,
|
||||
info: false, // Hide "Showing X of Y entries"
|
||||
responsive: true,
|
||||
dom: '<"top"f>rt<"bottom"p>', // Only show search and pagination
|
||||
paging: false, // Disable pagination
|
||||
});
|
||||
};
|
||||
|
||||
var initEvents = function () {
|
||||
// Get work ID from URL
|
||||
var pathArray = window.location.pathname.split("/");
|
||||
workId = pathArray[pathArray.length - 2]; // work/{id}/set-prices
|
||||
|
||||
// Save single price with debouncing
|
||||
$(document).on("click", ".save-single", function () {
|
||||
var dealerId = $(this).data("dealer-id");
|
||||
|
||||
// Clear existing timeout for this dealer
|
||||
if (saveTimeout[dealerId]) {
|
||||
clearTimeout(saveTimeout[dealerId]);
|
||||
}
|
||||
|
||||
// Set new timeout to prevent rapid clicks
|
||||
saveTimeout[dealerId] = setTimeout(function () {
|
||||
saveSinglePrice(dealerId);
|
||||
}, 300); // 300ms delay
|
||||
});
|
||||
|
||||
// Delete price
|
||||
$(document).on("click", ".delete-price", function () {
|
||||
var priceId = $(this).data("price-id");
|
||||
var dealerId = $(this).data("dealer-id");
|
||||
deletePrice(priceId, dealerId);
|
||||
});
|
||||
|
||||
// Save all prices
|
||||
$("#btn-save-all").on("click", function () {
|
||||
saveAllPrices();
|
||||
});
|
||||
|
||||
// Status toggle
|
||||
$(document).on("change", ".status-input", function () {
|
||||
var dealerId = $(this).data("dealer-id");
|
||||
var isChecked = $(this).is(":checked");
|
||||
var label = $(this).siblings("label");
|
||||
var checkbox = $(this);
|
||||
|
||||
// Update visual immediately
|
||||
if (isChecked) {
|
||||
label.text("Aktif").removeClass("inactive").addClass("active");
|
||||
} else {
|
||||
label
|
||||
.text("Nonaktif")
|
||||
.removeClass("active")
|
||||
.addClass("inactive");
|
||||
}
|
||||
|
||||
// Send AJAX request to update database
|
||||
toggleStatus(dealerId, isChecked, checkbox, label);
|
||||
});
|
||||
|
||||
// Format price input with thousand separator
|
||||
$(document).on("input", ".price-input", function () {
|
||||
var input = $(this);
|
||||
var value = input.val().replace(/[^\d]/g, "");
|
||||
|
||||
if (value === "") {
|
||||
input.val("0");
|
||||
} else {
|
||||
var numValue = parseInt(value);
|
||||
input.val(numValue.toLocaleString("id-ID"));
|
||||
// Don't update original value here - let it be updated only when saving
|
||||
}
|
||||
});
|
||||
|
||||
// Format price inputs on page load
|
||||
$(".price-input").each(function () {
|
||||
var input = $(this);
|
||||
var value = input.val();
|
||||
if (value && value !== "0") {
|
||||
var numValue = parseInt(value.replace(/[^\d]/g, ""));
|
||||
input.val(numValue.toLocaleString("id-ID"));
|
||||
// Store the original numeric value for comparison
|
||||
input.data("original-value", numValue.toString());
|
||||
console.log(
|
||||
"Initialized price for dealer",
|
||||
input.attr("name").replace("price_", ""),
|
||||
":",
|
||||
numValue
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var saveSinglePrice = function (dealerId) {
|
||||
// Prevent multiple clicks
|
||||
var saveButton = $('.save-single[data-dealer-id="' + dealerId + '"]');
|
||||
if (saveButton.hasClass("loading")) {
|
||||
return; // Already processing
|
||||
}
|
||||
|
||||
var priceInput = $('input[name="price_' + dealerId + '"]');
|
||||
var statusInput = $('input[name="status_' + dealerId + '"]');
|
||||
var price = priceInput.val().replace(/[^\d]/g, ""); // Remove non-numeric characters
|
||||
var isActive = statusInput.is(":checked");
|
||||
|
||||
if (!price || parseInt(price) <= 0) {
|
||||
toastr.error("Harga harus lebih dari 0");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get original price from data attribute (without separator)
|
||||
var originalPrice = priceInput.data("original-value") || "0";
|
||||
var currentPrice = parseInt(price);
|
||||
var originalPriceInt = parseInt(originalPrice);
|
||||
|
||||
console.log(
|
||||
"Debug - Original price:",
|
||||
originalPriceInt,
|
||||
"Current price:",
|
||||
currentPrice
|
||||
);
|
||||
|
||||
// If price hasn't actually changed, don't update
|
||||
if (currentPrice === originalPriceInt && originalPrice !== "0") {
|
||||
toastr.info("Harga tidak berubah, tidak perlu update");
|
||||
return;
|
||||
}
|
||||
|
||||
// If price has changed, update original value for next comparison
|
||||
if (currentPrice !== originalPriceInt) {
|
||||
priceInput.data("original-value", currentPrice.toString());
|
||||
console.log(
|
||||
"Price changed from",
|
||||
originalPriceInt,
|
||||
"to",
|
||||
currentPrice
|
||||
);
|
||||
}
|
||||
|
||||
// Disable button and show loading state
|
||||
saveButton.addClass("loading").prop("disabled", true);
|
||||
var originalText = saveButton.text();
|
||||
saveButton.text("Menyimpan...");
|
||||
|
||||
var data = {
|
||||
work_id: parseInt(workId),
|
||||
dealer_id: parseInt(dealerId),
|
||||
price: currentPrice, // Use the validated price
|
||||
currency: "IDR",
|
||||
is_active: isActive ? 1 : 0,
|
||||
};
|
||||
|
||||
// Debug: Log the data being sent
|
||||
console.log("Sending data:", data);
|
||||
console.log("Original price:", originalPriceInt);
|
||||
console.log("Current price:", currentPrice);
|
||||
|
||||
$.ajax({
|
||||
url: "/admin/work/" + workId + "/prices",
|
||||
method: "POST",
|
||||
data: data,
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
|
||||
},
|
||||
beforeSend: function () {
|
||||
console.log("Sending AJAX request with data:", data);
|
||||
},
|
||||
success: function (response) {
|
||||
// Re-enable button
|
||||
saveButton
|
||||
.removeClass("loading")
|
||||
.prop("disabled", false)
|
||||
.text(originalText);
|
||||
|
||||
if (response.status === 200) {
|
||||
toastr.success(response.message);
|
||||
// Update UI
|
||||
updateRowAfterSave(dealerId, response.data);
|
||||
|
||||
// Ensure consistent formatting after update
|
||||
var updatedPrice = priceInput.val().replace(/[^\d]/g, "");
|
||||
if (updatedPrice && updatedPrice !== "0") {
|
||||
var formattedPrice =
|
||||
parseInt(updatedPrice).toLocaleString("id-ID");
|
||||
priceInput.val(formattedPrice);
|
||||
}
|
||||
|
||||
// Show brief loading message
|
||||
toastr.info(
|
||||
"Data berhasil disimpan, memperbarui tampilan..."
|
||||
);
|
||||
} else {
|
||||
toastr.error(response.message || "Terjadi kesalahan");
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
// Re-enable button
|
||||
saveButton
|
||||
.removeClass("loading")
|
||||
.prop("disabled", false)
|
||||
.text(originalText);
|
||||
|
||||
var message = "Terjadi kesalahan";
|
||||
if (xhr.responseJSON) {
|
||||
if (xhr.responseJSON.message) {
|
||||
message = xhr.responseJSON.message;
|
||||
}
|
||||
if (xhr.responseJSON.errors) {
|
||||
// Show validation errors
|
||||
var errorMessages = [];
|
||||
for (var field in xhr.responseJSON.errors) {
|
||||
var fieldName = field;
|
||||
switch (field) {
|
||||
case "work_id":
|
||||
fieldName = "ID Pekerjaan";
|
||||
break;
|
||||
case "dealer_id":
|
||||
fieldName = "ID Dealer";
|
||||
break;
|
||||
case "price":
|
||||
fieldName = "Harga";
|
||||
break;
|
||||
case "currency":
|
||||
fieldName = "Mata Uang";
|
||||
break;
|
||||
case "is_active":
|
||||
fieldName = "Status Aktif";
|
||||
break;
|
||||
}
|
||||
errorMessages.push(
|
||||
fieldName +
|
||||
": " +
|
||||
xhr.responseJSON.errors[field][0]
|
||||
);
|
||||
}
|
||||
message = errorMessages.join("\n");
|
||||
}
|
||||
}
|
||||
toastr.error(message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
var saveAllPrices = function () {
|
||||
// Prevent multiple clicks
|
||||
var saveAllButton = $("#btn-save-all");
|
||||
if (saveAllButton.hasClass("loading")) {
|
||||
return; // Already processing
|
||||
}
|
||||
|
||||
var prices = [];
|
||||
var hasValidPrice = false;
|
||||
|
||||
$(".price-input").each(function () {
|
||||
var dealerId = $(this).attr("name").replace("price_", "");
|
||||
var price = $(this).val().replace(/[^\d]/g, ""); // Remove non-numeric characters
|
||||
var statusInput = $('input[name="status_' + dealerId + '"]');
|
||||
var isActive = statusInput.is(":checked");
|
||||
|
||||
if (price && parseInt(price) > 0) {
|
||||
hasValidPrice = true;
|
||||
prices.push({
|
||||
dealer_id: dealerId,
|
||||
price: parseInt(price),
|
||||
currency: "IDR",
|
||||
is_active: isActive,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasValidPrice) {
|
||||
toastr.error("Minimal satu dealer harus memiliki harga yang valid");
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable button and show loading state
|
||||
saveAllButton.addClass("loading").prop("disabled", true);
|
||||
var originalText = saveAllButton.text();
|
||||
saveAllButton.text("Menyimpan...");
|
||||
|
||||
// Show confirmation
|
||||
$("#confirmMessage").text(
|
||||
"Apakah Anda yakin ingin menyimpan semua harga?"
|
||||
);
|
||||
$("#confirmModal").modal("show");
|
||||
|
||||
$("#confirmAction")
|
||||
.off("click")
|
||||
.on("click", function () {
|
||||
$("#confirmModal").modal("hide");
|
||||
|
||||
$.ajax({
|
||||
url: "/admin/work/" + workId + "/prices/bulk",
|
||||
method: "POST",
|
||||
data: {
|
||||
prices: prices,
|
||||
},
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr(
|
||||
"content"
|
||||
),
|
||||
},
|
||||
success: function (response) {
|
||||
// Re-enable button
|
||||
saveAllButton
|
||||
.removeClass("loading")
|
||||
.prop("disabled", false)
|
||||
.text(originalText);
|
||||
|
||||
if (response.status === 200) {
|
||||
toastr.success(response.message);
|
||||
|
||||
// Show loading overlay
|
||||
showLoadingOverlay("Memperbarui data...");
|
||||
|
||||
// Reload page to update all data
|
||||
setTimeout(function () {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
toastr.error(
|
||||
response.message || "Terjadi kesalahan"
|
||||
);
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
// Re-enable button
|
||||
saveAllButton
|
||||
.removeClass("loading")
|
||||
.prop("disabled", false)
|
||||
.text(originalText);
|
||||
|
||||
var message = "Terjadi kesalahan";
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
message = xhr.responseJSON.message;
|
||||
}
|
||||
toastr.error(message);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var deletePrice = function (priceId, dealerId) {
|
||||
$("#confirmMessage").text(
|
||||
"Apakah Anda yakin ingin menghapus harga ini? Harga yang dihapus dapat dipulihkan dengan menyimpan ulang."
|
||||
);
|
||||
$("#confirmModal").modal("show");
|
||||
|
||||
$("#confirmAction")
|
||||
.off("click")
|
||||
.on("click", function () {
|
||||
$("#confirmModal").modal("hide");
|
||||
|
||||
// Show loading overlay
|
||||
showLoadingOverlay("Menghapus harga...");
|
||||
|
||||
$.ajax({
|
||||
url: "/admin/work/" + workId + "/prices/" + priceId,
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr(
|
||||
"content"
|
||||
),
|
||||
},
|
||||
success: function (response) {
|
||||
hideLoadingOverlay();
|
||||
|
||||
if (response.status === 200) {
|
||||
toastr.success(response.message);
|
||||
// Reset the row
|
||||
resetRow(dealerId);
|
||||
} else {
|
||||
toastr.error(
|
||||
response.message || "Terjadi kesalahan"
|
||||
);
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
hideLoadingOverlay();
|
||||
|
||||
var message = "Terjadi kesalahan";
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
message = xhr.responseJSON.message;
|
||||
}
|
||||
toastr.error(message);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Handle delete button click
|
||||
$(document).on("click", ".delete-price", function () {
|
||||
var priceId = $(this).data("price-id");
|
||||
var dealerId = $(this).data("dealer-id");
|
||||
deletePrice(priceId, dealerId);
|
||||
});
|
||||
|
||||
var updateRowAfterSave = function (dealerId, data) {
|
||||
var row = $('[data-dealer-id="' + dealerId + '"]');
|
||||
var priceInput = row.find('input[name="price_' + dealerId + '"]');
|
||||
var statusInput = row.find('input[name="status_' + dealerId + '"]');
|
||||
var label = statusInput.siblings("label").find(".status-text");
|
||||
var actionCell = row.find("td:last");
|
||||
|
||||
// Update price input if data contains price
|
||||
if (data.price !== undefined) {
|
||||
// Only update if the price actually changed
|
||||
var currentDisplayValue = priceInput.val().replace(/[^\d]/g, "");
|
||||
var newPrice = parseInt(data.price);
|
||||
|
||||
if (parseInt(currentDisplayValue) !== newPrice) {
|
||||
priceInput.val(newPrice.toLocaleString("id-ID"));
|
||||
// Update the original value for future comparisons
|
||||
priceInput.data("original-value", newPrice.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a new record (price = 0), update the save button
|
||||
if (data.price === 0) {
|
||||
actionCell
|
||||
.find(".save-single")
|
||||
.text("Simpan")
|
||||
.removeClass("btn-warning")
|
||||
.addClass("btn-success");
|
||||
}
|
||||
|
||||
// Update status if data contains is_active
|
||||
if (data.is_active !== undefined) {
|
||||
statusInput.prop("checked", data.is_active);
|
||||
if (data.is_active) {
|
||||
label.text("Aktif").removeClass("inactive").addClass("active");
|
||||
} else {
|
||||
label
|
||||
.text("Nonaktif")
|
||||
.removeClass("active")
|
||||
.addClass("inactive");
|
||||
}
|
||||
}
|
||||
|
||||
// Update save button if this is a new price save (not just status toggle)
|
||||
if (data.price !== undefined) {
|
||||
actionCell
|
||||
.find(".save-single")
|
||||
.text("Update")
|
||||
.removeClass("btn-success")
|
||||
.addClass("btn-warning");
|
||||
|
||||
// Update delete button
|
||||
if (actionCell.find(".delete-price").length === 0) {
|
||||
// Add delete button if it doesn't exist
|
||||
var deleteBtn =
|
||||
'<button type="button" class="btn btn-sm btn-danger delete-price" data-price-id="' +
|
||||
data.id +
|
||||
'" data-dealer-id="' +
|
||||
dealerId +
|
||||
'" title="Hapus Harga">Hapus</button>';
|
||||
actionCell.find(".d-flex.flex-row.gap-1").append(deleteBtn);
|
||||
} else {
|
||||
// Update existing delete button with new price ID
|
||||
actionCell.find(".delete-price").attr("data-price-id", data.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var toggleStatus = function (dealerId, isActive, checkbox, label) {
|
||||
var data = {
|
||||
dealer_id: parseInt(dealerId),
|
||||
is_active: isActive,
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: "/admin/work/" + workId + "/prices/toggle-status",
|
||||
method: "POST",
|
||||
data: data,
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
|
||||
},
|
||||
beforeSend: function () {
|
||||
// Show brief loading indicator on checkbox
|
||||
checkbox.prop("disabled", true);
|
||||
},
|
||||
success: function (response) {
|
||||
// Re-enable checkbox
|
||||
checkbox.prop("disabled", false);
|
||||
|
||||
if (response.status === 200) {
|
||||
toastr.success(response.message);
|
||||
|
||||
// Update UI if needed
|
||||
if (response.data) {
|
||||
// If this is a new record, update the row to show save button
|
||||
if (response.data.price === 0) {
|
||||
updateRowAfterSave(dealerId, response.data);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toastr.error(response.message || "Terjadi kesalahan");
|
||||
// Revert checkbox state
|
||||
checkbox.prop("checked", !isActive);
|
||||
if (!isActive) {
|
||||
label.text("Aktif");
|
||||
} else {
|
||||
label.text("Nonaktif");
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
// Re-enable checkbox
|
||||
checkbox.prop("disabled", false);
|
||||
|
||||
var message = "Terjadi kesalahan";
|
||||
if (xhr.responseJSON) {
|
||||
if (xhr.responseJSON.message) {
|
||||
message = xhr.responseJSON.message;
|
||||
}
|
||||
if (xhr.responseJSON.errors) {
|
||||
var errorMessages = [];
|
||||
for (var field in xhr.responseJSON.errors) {
|
||||
var fieldName = field;
|
||||
switch (field) {
|
||||
case "dealer_id":
|
||||
fieldName = "ID Dealer";
|
||||
break;
|
||||
case "is_active":
|
||||
fieldName = "Status Aktif";
|
||||
break;
|
||||
}
|
||||
errorMessages.push(
|
||||
fieldName +
|
||||
": " +
|
||||
xhr.responseJSON.errors[field][0]
|
||||
);
|
||||
}
|
||||
message = errorMessages.join("\n");
|
||||
}
|
||||
}
|
||||
toastr.error(message);
|
||||
|
||||
// Revert checkbox state
|
||||
checkbox.prop("checked", !isActive);
|
||||
if (!isActive) {
|
||||
label.text("Aktif");
|
||||
} else {
|
||||
label.text("Nonaktif");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
var resetRow = function (dealerId) {
|
||||
var row = $('[data-dealer-id="' + dealerId + '"]');
|
||||
var priceInput = row.find('input[name="price_' + dealerId + '"]');
|
||||
var statusInput = row.find('input[name="status_' + dealerId + '"]');
|
||||
var label = statusInput.siblings("label").find(".status-text");
|
||||
var actionCell = row.find("td:last");
|
||||
|
||||
// Reset price input
|
||||
priceInput.val("0");
|
||||
|
||||
// Reset status
|
||||
statusInput.prop("checked", false);
|
||||
label.text("Nonaktif").removeClass("active").addClass("inactive");
|
||||
|
||||
// Remove delete button and update save button
|
||||
actionCell.find(".delete-price").remove();
|
||||
actionCell
|
||||
.find(".save-single")
|
||||
.text("Simpan")
|
||||
.removeClass("btn-warning")
|
||||
.addClass("btn-success");
|
||||
};
|
||||
|
||||
// Public methods
|
||||
return {
|
||||
init: function () {
|
||||
initTable();
|
||||
initEvents();
|
||||
// Initialize price formatting on page load
|
||||
setTimeout(function () {
|
||||
$(".price-input").each(function () {
|
||||
var value = $(this).val();
|
||||
if (value && value !== "0") {
|
||||
var numValue = parseInt(value.replace(/[^\d]/g, ""));
|
||||
if (!isNaN(numValue)) {
|
||||
$(this).val(numValue.toLocaleString("id-ID"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
|
||||
// Cleanup timeouts on page unload
|
||||
$(window).on("beforeunload", function () {
|
||||
for (var dealerId in saveTimeout) {
|
||||
if (saveTimeout[dealerId]) {
|
||||
clearTimeout(saveTimeout[dealerId]);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
// On document ready
|
||||
jQuery(document).ready(function () {
|
||||
WorkPrices.init();
|
||||
});
|
||||
322
resources/views/back/master/work_prices.blade.php
Normal file
322
resources/views/back/master/work_prices.blade.php
Normal file
@@ -0,0 +1,322 @@
|
||||
@extends('layouts.backapp')
|
||||
|
||||
@section('content')
|
||||
|
||||
<style type="text/css">
|
||||
/* Action button flex layout */
|
||||
.d-flex.flex-row.gap-1 {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
gap: 0.25rem !important;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.d-flex.flex-row.gap-1 .btn {
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.d-flex.flex-row.gap-1 .btn i {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
/* Ensure DataTables doesn't break flex layout */
|
||||
.dataTables_wrapper .dataTables_scrollBody .d-flex {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.d-flex.flex-row.gap-1 {
|
||||
flex-direction: column !important;
|
||||
gap: 0.125rem !important;
|
||||
}
|
||||
|
||||
.d-flex.flex-row.gap-1 .btn {
|
||||
width: 100%;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Form control styling */
|
||||
.form-control {
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid #ced4da;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 0.375rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.btn i {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Status text styling */
|
||||
.status-text {
|
||||
font-weight: 600;
|
||||
transition: color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.status-text.active {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.status-text.inactive {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
/* Input group styling */
|
||||
.input-group-text {
|
||||
background-color: #e9ecef;
|
||||
border: 1px solid #ced4da;
|
||||
color: #495057;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Table styling */
|
||||
.table th {
|
||||
background-color: #f8f9fa;
|
||||
border-color: #dee2e6;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Price input styling */
|
||||
.price-input {
|
||||
text-align: right;
|
||||
font-weight: 500;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.price-input:focus {
|
||||
border-color: #80bdff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
/* Status label styling */
|
||||
.custom-control-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Table styling */
|
||||
.table th {
|
||||
background-color: #f8f9fa;
|
||||
border-color: #dee2e6;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Status label styling */
|
||||
.custom-control-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Alert styling */
|
||||
.alert {
|
||||
border-radius: 0.5rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
/* Button loading state */
|
||||
.btn.loading {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Loading overlay */
|
||||
#loading-overlay {
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
#loading-overlay .spinner-border {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="kt-portlet kt-portlet--mobile" id="kt_blockui_datatable">
|
||||
<div class="kt-portlet__head kt-portlet__head--lg">
|
||||
<div class="kt-portlet__head-label">
|
||||
<span class="kt-portlet__head-icon">
|
||||
<i class="kt-font-brand flaticon2-line-chart"></i>
|
||||
</span>
|
||||
<h3 class="kt-portlet__head-title">
|
||||
Set Harga Pekerjaan: <strong>{{ $work->name }}</strong>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="kt-portlet__head-toolbar">
|
||||
<div class="kt-portlet__head-wrapper">
|
||||
<div class="kt-portlet__head-actions">
|
||||
<button type="button" class="btn btn-bold btn-label-brand" id="btn-save-all">
|
||||
Simpan Semua Harga
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kt-portlet__body">
|
||||
|
||||
<div class="table-responsive">
|
||||
<!--begin: Datatable -->
|
||||
<table class="table table-striped table-bordered table-hover table-checkable" id="dealers-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="5%">No</th>
|
||||
<th width="25%">Nama Dealer</th>
|
||||
<th width="15%">Kode Dealer</th>
|
||||
<th width="20%">Harga (IDR)</th>
|
||||
<th width="10%">Status</th>
|
||||
<th width="15%">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($dealers as $index => $dealer)
|
||||
@php
|
||||
$existingPrice = $work->getPriceForDealer($dealer->id);
|
||||
@endphp
|
||||
<tr data-dealer-id="{{ $dealer->id }}">
|
||||
<td>{{ $index + 1 }}</td>
|
||||
<td>{{ $dealer->name }}</td>
|
||||
<td>{{ $dealer->dealer_code }}</td>
|
||||
<td>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Rp</span>
|
||||
</div>
|
||||
<input type="text"
|
||||
class="form-control price-input"
|
||||
name="price_{{ $dealer->id }}"
|
||||
value="{{ $existingPrice ? number_format($existingPrice->price, 0, ',', '.') : '0' }}"
|
||||
placeholder="0"
|
||||
data-dealer-id="{{ $dealer->id }}"
|
||||
data-original-value="{{ $existingPrice ? $existingPrice->price : '0' }}">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
class="status-input"
|
||||
id="status_{{ $dealer->id }}"
|
||||
name="status_{{ $dealer->id }}"
|
||||
data-dealer-id="{{ $dealer->id }}"
|
||||
{{ $existingPrice && $existingPrice->is_active ? 'checked' : '' }}>
|
||||
<label for="status_{{ $dealer->id }}" style="margin-left: 0.5rem; font-weight: 500;">Aktif</label>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-row gap-1">
|
||||
<button type="button"
|
||||
class="btn btn-sm {{ $existingPrice ? 'btn-warning' : 'btn-success' }} save-single"
|
||||
data-dealer-id="{{ $dealer->id }}"
|
||||
title="{{ $existingPrice ? 'Update Harga' : 'Simpan Harga' }}">
|
||||
{{ $existingPrice ? 'Update' : 'Simpan' }}
|
||||
</button>
|
||||
@if($existingPrice)
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-danger delete-price"
|
||||
data-price-id="{{ $existingPrice->id }}"
|
||||
data-dealer-id="{{ $dealer->id }}"
|
||||
title="Hapus Harga">
|
||||
Hapus
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
<!--end: Datatable -->
|
||||
</div>
|
||||
|
||||
@if($dealers->count() == 0)
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning text-center">
|
||||
<i class="fa fa-exclamation-triangle mr-2"></i>
|
||||
Tidak ada dealer yang tersedia.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--begin::Modal-->
|
||||
<div class="modal fade" id="confirmModal" tabindex="-1" role="dialog" aria-labelledby="confirmModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="confirmModalLabel">
|
||||
<i class="fa fa-question-circle mr-2"></i>
|
||||
Konfirmasi
|
||||
</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="confirmMessage"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">
|
||||
<i class="fa fa-times mr-1"></i>
|
||||
Batal
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" id="confirmAction">
|
||||
<i class="fa fa-check mr-1"></i>
|
||||
Ya, Lanjutkan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--end::Modal-->
|
||||
|
||||
@endsection
|
||||
|
||||
@section('javascripts')
|
||||
<script src="{{ url('js/pages/back/master/work-prices.js') }}" type="text/javascript"></script>
|
||||
@endsection
|
||||
@@ -24,22 +24,24 @@
|
||||
</div>
|
||||
|
||||
<div class="kt-portlet__body">
|
||||
<!--begin: Datatable -->
|
||||
<table class="table table-responsive table-striped table-bordered table-hover table-checkable" id="kt_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<th>Dealer</th>
|
||||
<th>Role</th>
|
||||
<th style="width: 35%;">Nama Pengguna</th>
|
||||
<th>Email</th>
|
||||
<th>Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
<!--end: Datatable -->
|
||||
<div class="table-responsive">
|
||||
<!--begin: Datatable -->
|
||||
<table class="table table-striped table-bordered table-hover table-checkable" id="kt_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<th>Dealer</th>
|
||||
<th>Role</th>
|
||||
<th style="width: 35%;">Nama Pengguna</th>
|
||||
<th>Email</th>
|
||||
<th>Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
<!--end: Datatable -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
@if(Gate::check('view', $menus['user.index']) || Gate::check('view', $menus['roleprivileges.index']))
|
||||
<div class="kt-menu__section" style="padding: 15px 20px; margin-top: 10px; margin-bottom: 5px;">
|
||||
<div style="display: flex; align-items: center; color: #a7abc3; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||
<i class="fa fa-users" style="margin-right: 8px; font-size: 14px;"></i>
|
||||
{{-- <i class="fa fa-users" style="margin-right: 8px; font-size: 14px;"></i> --}}
|
||||
<span>Manajemen Pengguna</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,7 +77,7 @@
|
||||
@if(Gate::check('view', $menus['work.index']) || Gate::check('view', $menus['category.index']) || Gate::check('view', $menus['dealer.index']))
|
||||
<div class="kt-menu__section" style="padding: 15px 20px; margin-top: 10px; margin-bottom: 5px;">
|
||||
<div style="display: flex; align-items: center; color: #a7abc3; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||
<i class="fa fa-exchange-alt" style="margin-right: 8px; font-size: 14px;"></i>
|
||||
{{-- <i class="fa fa-exchange-alt" style="margin-right: 8px; font-size: 14px;"></i> --}}
|
||||
<span>Manajemen Transaksi</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,7 +115,7 @@
|
||||
@if(Gate::check('view', $menus['products.index']) || Gate::check('view', $menus['product_categories.index']) || Gate::check('view', $menus['mutations.index']) || Gate::check('view', $menus['opnames.index']) || Gate::check('view', $menus['stock-audit.index']))
|
||||
<div class="kt-menu__section" style="padding: 15px 20px; margin-top: 10px; margin-bottom: 5px;">
|
||||
<div style="display: flex; align-items: center; color: #a7abc3; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||
<i class="fa fa-warehouse" style="margin-right: 8px; font-size: 14px;"></i>
|
||||
{{-- <i class="fa fa-warehouse" style="margin-right: 8px; font-size: 14px;"></i> --}}
|
||||
<span>Manajemen Gudang</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -171,7 +171,7 @@
|
||||
@if(Gate::check('view', $menus['report.transaction_sa']) || Gate::check('view', $menus['report.transaction']) || Gate::check('view', $menus['report.transaction_dealer']) || Gate::check('view', $menus['work.index']))
|
||||
<div class="kt-menu__section" style="padding: 15px 20px; margin-top: 10px; margin-bottom: 5px;">
|
||||
<div style="display: flex; align-items: center; color: #a7abc3; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||
<i class="fa fa-chart-bar" style="margin-right: 8px; font-size: 14px;"></i>
|
||||
{{-- <i class="fa fa-chart-bar" style="margin-right: 8px; font-size: 14px;"></i> --}}
|
||||
<span>Laporan</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -223,13 +223,17 @@
|
||||
</li>
|
||||
@endcan
|
||||
|
||||
{{-- Section Header - Only show if user has access to any submenu --}}
|
||||
@if(Gate::check('view', $menus['kpi.targets.index']))
|
||||
<div class="kt-menu__section" style="padding: 15px 20px; margin-top: 10px; margin-bottom: 5px;">
|
||||
<div style="display: flex; align-items: center; color: #a7abc3; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||
<i class="fa fa-user-cog" style="margin-right: 8px; font-size: 14px;"></i>
|
||||
{{-- <i class="fa fa-user-cog" style="margin-right: 8px; font-size: 14px;"></i> --}}
|
||||
<span>KPI</span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Submenu Items --}}
|
||||
@can('view', $menus['kpi.targets.index'])
|
||||
<li class="kt-menu__item" aria-haspopup="true">
|
||||
<a href="{{ route('kpi.targets.index') }}" class="kt-menu__link">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ use App\Http\Controllers\WarehouseManagement\OpnamesController;
|
||||
use App\Http\Controllers\WarehouseManagement\ProductCategoriesController;
|
||||
use App\Http\Controllers\WarehouseManagement\ProductsController;
|
||||
use App\Http\Controllers\WorkController;
|
||||
use App\Http\Controllers\WorkDealerPriceController;
|
||||
use App\Http\Controllers\WarehouseManagement\MutationsController;
|
||||
use App\Http\Controllers\WarehouseManagement\StockAuditController;
|
||||
use App\Http\Controllers\KPI\TargetsController;
|
||||
@@ -166,8 +167,15 @@ Route::group(['middleware' => 'auth'], function() {
|
||||
// Stock Management Routes
|
||||
Route::post('/transaction/check-stock', [TransactionController::class, 'checkStockAvailability'])->name('transaction.check-stock');
|
||||
Route::get('/transaction/stock-prediction', [TransactionController::class, 'getStockPrediction'])->name('transaction.stock-prediction');
|
||||
|
||||
// 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');
|
||||
});
|
||||
|
||||
// KPI Data Route - accessible to all authenticated users
|
||||
Route::get('/transaction/get-kpi-data', [TransactionController::class, 'getKpiData'])->name('transaction.get-kpi-data');
|
||||
|
||||
Route::group(['prefix' => 'admin', 'middleware' => 'adminRole'], function() {
|
||||
Route::get('/dashboard2', [AdminController::class, 'dashboard2'])->name('dashboard2');
|
||||
Route::post('/dealer_work_trx', [AdminController::class, 'dealer_work_trx'])->name('dealer_work_trx');
|
||||
@@ -182,6 +190,23 @@ Route::group(['middleware' => 'auth'], function() {
|
||||
Route::resource('category', CategoryController::class);
|
||||
Route::resource('work', WorkController::class);
|
||||
|
||||
// Work Dealer Prices Routes
|
||||
Route::prefix('work/{work}/prices')->name('work.prices.')->controller(WorkDealerPriceController::class)->group(function () {
|
||||
Route::get('/', 'index')->name('index');
|
||||
Route::post('/', 'store')->name('store');
|
||||
Route::get('{price}/edit', 'edit')->name('edit');
|
||||
Route::put('{price}', 'update')->name('update');
|
||||
Route::delete('{price}', 'destroy')->name('destroy');
|
||||
Route::post('bulk', 'bulkCreate')->name('bulk-create');
|
||||
Route::post('toggle-status', 'toggleStatus')->name('toggle-status');
|
||||
});
|
||||
|
||||
// Route untuk halaman set harga (menggunakan WorkController)
|
||||
Route::get('work/{work}/set-prices', [WorkController::class, 'showPrices'])->name('work.set-prices');
|
||||
|
||||
// API route untuk mendapatkan harga
|
||||
Route::get('work/get-price', [WorkDealerPriceController::class, 'getPrice'])->name('work.get-price');
|
||||
|
||||
// Work Products Management Routes
|
||||
Route::prefix('work/{work}/products')->name('work.products.')->controller(App\Http\Controllers\WorkProductController::class)->group(function () {
|
||||
Route::get('/', 'index')->name('index');
|
||||
|
||||
Reference in New Issue
Block a user