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