create feature sa create list claim and price to work per dealer

This commit is contained in:
2025-07-07 19:11:04 +07:00
parent fa554446ca
commit 956df5cfe6
16 changed files with 3062 additions and 404 deletions

View File

@@ -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);
}
}
}

View File

@@ -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'));
}
}

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

View File

@@ -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();
}
}

View File

@@ -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',
];
/**

View File

@@ -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();
}
}

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

View File

@@ -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
];
}
}

View File

@@ -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']);
});
}
}

View File

@@ -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');
}
}

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

View 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">&times;</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

View File

@@ -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>

View File

@@ -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

View File

@@ -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');