partial update create kpi and progress bar

This commit is contained in:
2025-07-04 18:27:32 +07:00
parent 0ef03fe7cb
commit fa554446ca
19 changed files with 2150 additions and 45 deletions

View File

@@ -0,0 +1,237 @@
<?php
namespace App\Http\Controllers\KPI;
use App\Http\Controllers\Controller;
use App\Http\Requests\KPI\StoreKpiTargetRequest;
use App\Http\Requests\KPI\UpdateKpiTargetRequest;
use App\Models\KpiTarget;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Log;
class TargetsController extends Controller
{
/**
* Display a listing of KPI targets
*/
public function index()
{
$targets = KpiTarget::with(['user', 'user.role'])
->orderBy('created_at', 'desc')
->paginate(15);
// Get mechanics using role_id 3 (mechanic) with dealer relationship
$mechanics = User::with('dealer')
->where('role_id', 3)
->orderBy('name', 'asc')
->limit(50)
->get();
// If no mechanics found, get all users as fallback
if ($mechanics->isEmpty()) {
$mechanics = User::with('dealer')
->orderBy('name', 'asc')
->limit(50)
->get();
}
return view('kpi.targets.index', compact('targets', 'mechanics'));
}
/**
* Show the form for creating a new KPI target
*/
public function create()
{
// Get mechanics using role_id 3 (mechanic) with dealer relationship
$mechanics = User::with('dealer')
->where('role_id', 3)
->orderBy('name', 'asc')
->limit(50)
->get();
// Debug: Log the mechanics found
Log::info('Mechanics found for KPI target creation:', [
'count' => $mechanics->count(),
'mechanics' => $mechanics->pluck('name', 'id')->toArray()
]);
// If no mechanics found, get all users as fallback
if ($mechanics->isEmpty()) {
$mechanics = User::with('dealer')
->orderBy('name', 'asc')
->limit(50)
->get();
Log::warning('No mechanics found, using all users as fallback', [
'count' => $mechanics->count()
]);
}
return view('kpi.targets.create', compact('mechanics'));
}
/**
* Store a newly created KPI target
*/
public function store(StoreKpiTargetRequest $request)
{
try {
// Log the validated data
Log::info('Creating KPI target with data:', $request->validated());
// Check if user already has an active target and deactivate it
$existingTarget = KpiTarget::where('user_id', $request->user_id)
->where('is_active', true)
->first();
if ($existingTarget) {
Log::info('Deactivating existing active KPI target', [
'user_id' => $request->user_id,
'existing_target_id' => $existingTarget->id
]);
// Deactivate the existing target
$existingTarget->update(['is_active' => false]);
}
$target = KpiTarget::create($request->validated());
Log::info('KPI target created successfully', [
'target_id' => $target->id,
'user_id' => $target->user_id
]);
return redirect()->route('kpi.targets.index')
->with('success', 'Target KPI berhasil ditambahkan');
} catch (\Exception $e) {
Log::error('Failed to create KPI target', [
'error' => $e->getMessage(),
'data' => $request->validated()
]);
return redirect()->back()
->withInput()
->with('error', 'Gagal menambahkan target KPI: ' . $e->getMessage());
}
}
/**
* Display the specified KPI target
*/
public function show(KpiTarget $target)
{
$target->load(['user.dealer', 'achievements']);
return view('kpi.targets.show', compact('target'));
}
/**
* Show the form for editing the specified KPI target
*/
public function edit(KpiTarget $target)
{
// Debug: Check if target is loaded correctly
if (!$target) {
abort(404, 'Target KPI tidak ditemukan');
}
// Load target with user relationship
$target->load('user');
// Get mechanics using role_id 3 (mechanic) with dealer relationship
$mechanics = User::with('dealer')
->where('role_id', 3)
->orderBy('name', 'asc')
->limit(50)
->get();
// If no mechanics found, get all users as fallback
if ($mechanics->isEmpty()) {
$mechanics = User::with('dealer')
->orderBy('name', 'asc')
->limit(50)
->get();
}
// Ensure data types are correct for comparison
$target->user_id = (int)$target->user_id;
return view('kpi.targets.edit', compact('target', 'mechanics'));
}
/**
* Update the specified KPI target
*/
public function update(UpdateKpiTargetRequest $request, KpiTarget $target)
{
try {
$target->update($request->validated());
return redirect()->route('kpi.targets.index')
->with('success', 'Target KPI berhasil diperbarui');
} catch (\Exception $e) {
return redirect()->back()
->withInput()
->with('error', 'Gagal memperbarui target KPI: ' . $e->getMessage());
}
}
/**
* Remove the specified KPI target
*/
public function destroy(KpiTarget $target)
{
try {
$target->delete();
return redirect()->route('kpi.targets.index')
->with('success', 'Target KPI berhasil dihapus');
} catch (\Exception $e) {
return redirect()->back()
->with('error', 'Gagal menghapus target KPI: ' . $e->getMessage());
}
}
/**
* Toggle active status of KPI target
*/
public function toggleStatus(KpiTarget $target)
{
try {
$target->update(['is_active' => !$target->is_active]);
$status = $target->is_active ? 'diaktifkan' : 'dinonaktifkan';
return response()->json([
'success' => true,
'message' => "Target KPI berhasil {$status}",
'is_active' => $target->is_active
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Gagal mengubah status target KPI'
], 500);
}
}
/**
* Get KPI targets for specific user
*/
public function getUserTargets(User $user)
{
$targets = $user->kpiTargets()
->with('achievements')
->orderBy('year', 'desc')
->orderBy('month', 'desc')
->get();
return response()->json([
'success' => true,
'data' => $targets
]);
}
}

View File

@@ -49,7 +49,28 @@ class TransactionController extends Controller
->where('active', true)
->get();
return view('transaction.index', compact('now', 'wash_work', 'work_works', 'user_sas', 'count_transaction_users', 'count_transaction_dealers', 'mechanic', 'products'));
// 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());
$kpiSummary = $kpiService->getKpiSummary(Auth::user());
// Get current month period name
$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 view('transaction.index', compact('now', 'wash_work', 'work_works', 'user_sas', 'count_transaction_users', 'count_transaction_dealers', 'mechanic', 'products', 'kpiData'));
}
public function workcategory($category_id)

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Http\Requests\KPI;
use Illuminate\Foundation\Http\FormRequest;
use Carbon\Carbon;
class StoreKpiTargetRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'user_id' => 'required|exists:users,id',
'target_value' => 'required|integer|min:1',
'description' => 'nullable|string|max:1000',
'is_active' => 'boolean'
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'user_id.required' => 'Mekanik harus dipilih',
'user_id.exists' => 'Mekanik yang dipilih tidak valid',
'target_value.required' => 'Target nilai harus diisi',
'target_value.integer' => 'Target nilai harus berupa angka',
'target_value.min' => 'Target nilai minimal 1',
'description.max' => 'Deskripsi maksimal 1000 karakter',
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
$this->merge([
'is_active' => $this->boolean('is_active', true)
]);
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Http\Requests\KPI;
use Illuminate\Foundation\Http\FormRequest;
use Carbon\Carbon;
class UpdateKpiTargetRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'user_id' => 'required|exists:users,id',
'target_value' => 'required|integer|min:1',
'description' => 'nullable|string|max:1000',
'is_active' => 'boolean'
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'user_id.required' => 'Mekanik harus dipilih',
'user_id.exists' => 'Mekanik yang dipilih tidak valid',
'target_value.required' => 'Target nilai harus diisi',
'target_value.integer' => 'Target nilai harus berupa angka',
'target_value.min' => 'Target nilai minimal 1',
'description.max' => 'Deskripsi maksimal 1000 karakter',
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
$this->merge([
'is_active' => $this->boolean('is_active', true)
]);
}
}

View File

@@ -0,0 +1,168 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Carbon\Carbon;
class KpiAchievement extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'kpi_target_id',
'target_value',
'actual_value',
'achievement_percentage',
'year',
'month',
'notes'
];
protected $casts = [
'achievement_percentage' => 'decimal:2',
'year' => 'integer',
'month' => 'integer'
];
protected $attributes = [
'actual_value' => 0,
'achievement_percentage' => 0
];
/**
* Get the user that owns the achievement
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get the KPI target for this achievement
*/
public function kpiTarget(): BelongsTo
{
return $this->belongsTo(KpiTarget::class);
}
/**
* Scope to get achievements for specific year and month
*/
public function scopeForPeriod($query, $year, $month)
{
return $query->where('year', $year)->where('month', $month);
}
/**
* Scope to get achievements for current month
*/
public function scopeCurrentMonth($query)
{
return $query->where('year', now()->year)->where('month', now()->month);
}
/**
* Scope to get achievements within year range
*/
public function scopeWithinYearRange($query, $startYear, $endYear)
{
return $query->whereBetween('year', [$startYear, $endYear]);
}
/**
* Scope to get achievements for specific user
*/
public function scopeForUser($query, $userId)
{
return $query->where('user_id', $userId);
}
/**
* Get achievement status
*/
public function getStatusAttribute(): string
{
if ($this->achievement_percentage >= 100) {
return 'exceeded';
} elseif ($this->achievement_percentage >= 80) {
return 'good';
} elseif ($this->achievement_percentage >= 60) {
return 'fair';
} else {
return 'poor';
}
}
/**
* Get status color for display
*/
public function getStatusColorAttribute(): string
{
return match($this->status) {
'exceeded' => 'success',
'good' => 'info',
'fair' => 'warning',
'poor' => 'danger',
default => 'secondary'
};
}
/**
* Get period display name (e.g., "Januari 2024")
*/
public function getPeriodDisplayName(): string
{
$monthNames = [
1 => 'Januari', 2 => 'Februari', 3 => 'Maret', 4 => 'April',
5 => 'Mei', 6 => 'Juni', 7 => 'Juli', 8 => 'Agustus',
9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Desember'
];
return $monthNames[$this->month] . ' ' . $this->year;
}
/**
* Get period start date
*/
public function getPeriodStartDate(): Carbon
{
return Carbon::createFromDate($this->year, $this->month, 1);
}
/**
* Get period end date
*/
public function getPeriodEndDate(): Carbon
{
return Carbon::createFromDate($this->year, $this->month, 1)->endOfMonth();
}
/**
* Get target value (from stored value or from relation)
*/
public function getTargetValueAttribute(): int
{
// Return stored target value if available, otherwise get from relation
return $this->target_value ?? $this->kpiTarget?->target_value ?? 0;
}
/**
* Get current target value from relation (for comparison)
*/
public function getCurrentTargetValueAttribute(): int
{
return $this->kpiTarget?->target_value ?? 0;
}
/**
* Check if stored target value differs from current target value
*/
public function hasTargetValueChanged(): bool
{
return $this->target_value !== $this->current_target_value;
}
}

61
app/Models/KpiTarget.php Normal file
View File

@@ -0,0 +1,61 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Carbon\Carbon;
class KpiTarget extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'target_value',
'is_active',
'description'
];
protected $casts = [
'is_active' => 'boolean'
];
protected $attributes = [
'is_active' => true
];
/**
* Get the user that owns the KPI target
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get the achievements for this target
*/
public function achievements(): HasMany
{
return $this->hasMany(KpiAchievement::class);
}
/**
* Scope to get active targets
*/
public function scopeActive($query)
{
return $query->where('is_active', true);
}
/**
* Check if target is currently active
*/
public function isCurrentlyActive(): bool
{
return $this->is_active;
}
}

View File

@@ -132,4 +132,64 @@ class User extends Authenticatable
return false;
}
}
/**
* Get all KPI targets for the User
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function kpiTargets()
{
return $this->hasMany(KpiTarget::class);
}
/**
* Get all KPI achievements for the User
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function kpiAchievements()
{
return $this->hasMany(KpiAchievement::class);
}
/**
* Check if user is mechanic
*
* @return bool
*/
public function isMechanic()
{
return $this->hasRole('mechanic');
}
/**
* Get current KPI target (no longer filtered by year/month)
*
* @return KpiTarget|null
*/
public function getCurrentKpiTarget()
{
return $this->kpiTargets()
->where('is_active', true)
->first();
}
/**
* Get KPI achievement for specific year and month
*
* @param int $year
* @param int $month
* @return KpiAchievement|null
*/
public function getKpiAchievement($year = null, $month = null)
{
$year = $year ?? now()->year;
$month = $month ?? now()->month;
return $this->kpiAchievements()
->where('year', $year)
->where('month', $month)
->first();
}
}

332
app/Services/KpiService.php Normal file
View File

@@ -0,0 +1,332 @@
<?php
namespace App\Services;
use App\Models\User;
use App\Models\Transaction;
use App\Models\KpiTarget;
use App\Models\KpiAchievement;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class KpiService
{
/**
* Calculate KPI achievement for a user
*
* @param User $user
* @param int $year
* @param int $month
* @return KpiAchievement|null
*/
public function calculateKpiAchievement(User $user, $year = null, $month = null)
{
$year = $year ?? now()->year;
$month = $month ?? now()->month;
// Get current KPI target (no longer filtered by year/month)
$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 based on month
$actualValue = $this->getActualWorkCount($user, $year, $month);
// Calculate percentage
$achievementPercentage = $kpiTarget->target_value > 0
? ($actualValue / $kpiTarget->target_value) * 100
: 0;
// Save or update achievement with target value stored directly
return KpiAchievement::updateOrCreate(
[
'user_id' => $user->id,
'year' => $year,
'month' => $month
],
[
'kpi_target_id' => $kpiTarget->id,
'target_value' => $kpiTarget->target_value, // Store target value directly for historical tracking
'actual_value' => $actualValue,
'achievement_percentage' => $achievementPercentage
]
);
}
/**
* Get actual work count for a user in specific month
*
* @param User $user
* @param int $year
* @param int $month
* @return int
*/
private function getActualWorkCount(User $user, $year, $month)
{
return Transaction::where('user_id', $user->id)
->where('status', 'completed')
->whereYear('date', $year)
->whereMonth('date', $month)
->sum('qty');
}
/**
* Generate KPI report for a user
*
* @param User $user
* @param int|null $year
* @param int|null $month
* @return array
*/
public function generateKpiReport(User $user, $year = null, $month = null)
{
$year = $year ?? now()->year;
$month = $month ?? now()->month;
$achievements = $user->kpiAchievements()
->where('year', $year)
->where('month', $month)
->orderBy('month')
->get();
$target = $user->kpiTargets()
->where('is_active', true)
->first();
return [
'user' => $user,
'target' => $target,
'achievements' => $achievements,
'summary' => $this->calculateSummary($achievements),
'period' => [
'year' => $year,
'month' => $month,
'period_name' => $this->getMonthName($month) . ' ' . $year
]
];
}
/**
* Calculate summary statistics for achievements
*
* @param \Illuminate\Database\Eloquent\Collection $achievements
* @return array
*/
private function calculateSummary($achievements)
{
if ($achievements->isEmpty()) {
return [
'total_target' => 0,
'total_actual' => 0,
'average_achievement' => 0,
'best_period' => null,
'worst_period' => null,
'total_periods' => 0,
'achievement_rate' => 0
];
}
$totalTarget = $achievements->sum('target_value');
$totalActual = $achievements->sum('actual_value');
$averageAchievement = $achievements->avg('achievement_percentage');
$totalPeriods = $achievements->count();
$achievementRate = $totalPeriods > 0 ? ($achievements->where('achievement_percentage', '>=', 100)->count() / $totalPeriods) * 100 : 0;
$bestPeriod = $achievements->sortByDesc('achievement_percentage')->first();
$worstPeriod = $achievements->sortBy('achievement_percentage')->first();
return [
'total_target' => $totalTarget,
'total_actual' => $totalActual,
'average_achievement' => round($averageAchievement, 2),
'best_period' => $bestPeriod,
'worst_period' => $worstPeriod,
'total_periods' => $totalPeriods,
'achievement_rate' => round($achievementRate, 2)
];
}
/**
* Get KPI statistics for all mechanics
*
* @param int|null $year
* @param int|null $month
* @return array
*/
public function getMechanicsKpiStats($year = null, $month = null)
{
$year = $year ?? now()->year;
$month = $month ?? now()->month;
$mechanics = User::whereHas('role', function($query) {
$query->where('name', 'mechanic');
})->get();
$stats = [];
foreach ($mechanics as $mechanic) {
$report = $this->generateKpiReport($mechanic, $year, $month);
$stats[] = [
'user' => $mechanic,
'summary' => $report['summary'],
'target' => $report['target']
];
}
return $stats;
}
/**
* Auto-calculate KPI achievements for all mechanics
*
* @param int|null $year
* @param int|null $month
* @return array
*/
public function autoCalculateAllMechanics($year = null, $month = null)
{
$year = $year ?? now()->year;
$month = $month ?? now()->month;
$mechanics = User::whereHas('role', function($query) {
$query->where('name', 'mechanic');
})->get();
$results = [];
foreach ($mechanics as $mechanic) {
try {
$achievement = $this->calculateKpiAchievement($mechanic, $year, $month);
$results[] = [
'user_id' => $mechanic->id,
'user_name' => $mechanic->name,
'success' => true,
'achievement' => $achievement
];
} catch (\Exception $e) {
Log::error("Failed to calculate KPI for user {$mechanic->id}: " . $e->getMessage());
$results[] = [
'user_id' => $mechanic->id,
'user_name' => $mechanic->name,
'success' => false,
'error' => $e->getMessage()
];
}
}
return $results;
}
/**
* Get KPI trend data for chart
*
* @param User $user
* @param int $months
* @return array
*/
public function getKpiTrendData(User $user, $months = 12)
{
$endDate = now();
$startDate = $endDate->copy()->subMonths($months);
$achievements = $user->kpiAchievements()
->where(function($query) use ($startDate, $endDate) {
$query->where(function($q) use ($startDate, $endDate) {
$q->where('year', '>', $startDate->year)
->orWhere(function($subQ) use ($startDate, $endDate) {
$subQ->where('year', $startDate->year)
->where('month', '>=', $startDate->month);
});
})
->where(function($q) use ($endDate) {
$q->where('year', '<', $endDate->year)
->orWhere(function($subQ) use ($endDate) {
$subQ->where('year', $endDate->year)
->where('month', '<=', $endDate->month);
});
});
})
->orderBy('year')
->orderBy('month')
->get();
$trendData = [];
foreach ($achievements as $achievement) {
$trendData[] = [
'period' => $achievement->getPeriodDisplayName(),
'target' => $achievement->target_value,
'actual' => $achievement->actual_value,
'percentage' => $achievement->achievement_percentage,
'status' => $achievement->status
];
}
return $trendData;
}
/**
* Get month name in Indonesian
*
* @param int $month
* @return string
*/
private function getMonthName($month)
{
$monthNames = [
1 => 'Januari', 2 => 'Februari', 3 => 'Maret', 4 => 'April',
5 => 'Mei', 6 => 'Juni', 7 => 'Juli', 8 => 'Agustus',
9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Desember'
];
return $monthNames[$month] ?? 'Unknown';
}
/**
* Get KPI summary for dashboard
*
* @param User $user
* @return array
*/
public function getKpiSummary(User $user)
{
$currentYear = now()->year;
$currentMonth = now()->month;
// Get current month achievement
$currentAchievement = $user->kpiAchievements()
->where('year', $currentYear)
->where('month', $currentMonth)
->first();
// Get current month target (no longer filtered by year/month)
$currentTarget = $user->kpiTargets()
->where('is_active', true)
->first();
// Get last 6 months achievements
$recentAchievements = $user->kpiAchievements()
->where(function($query) use ($currentYear, $currentMonth) {
$query->where('year', '>', $currentYear - 1)
->orWhere(function($q) use ($currentYear, $currentMonth) {
$q->where('year', $currentYear)
->where('month', '>=', max(1, $currentMonth - 5));
});
})
->orderBy('year', 'desc')
->orderBy('month', 'desc')
->limit(6)
->get();
return [
'current_achievement' => $currentAchievement,
'current_target' => $currentTarget,
'recent_achievements' => $recentAchievements,
'current_percentage' => $currentAchievement ? $currentAchievement->achievement_percentage : 0,
'is_on_track' => $currentAchievement ? $currentAchievement->achievement_percentage >= 100 : false
];
}
}