partial update create kpi and progress bar
This commit is contained in:
237
app/Http/Controllers/KPI/TargetsController.php
Normal file
237
app/Http/Controllers/KPI/TargetsController.php
Normal 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
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,7 +49,28 @@ class TransactionController extends Controller
|
|||||||
->where('active', true)
|
->where('active', true)
|
||||||
->get();
|
->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)
|
public function workcategory($category_id)
|
||||||
|
|||||||
59
app/Http/Requests/KPI/StoreKpiTargetRequest.php
Normal file
59
app/Http/Requests/KPI/StoreKpiTargetRequest.php
Normal 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)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
59
app/Http/Requests/KPI/UpdateKpiTargetRequest.php
Normal file
59
app/Http/Requests/KPI/UpdateKpiTargetRequest.php
Normal 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)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
168
app/Models/KpiAchievement.php
Normal file
168
app/Models/KpiAchievement.php
Normal 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
61
app/Models/KpiTarget.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -132,4 +132,64 @@ class User extends Authenticatable
|
|||||||
return false;
|
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
332
app/Services/KpiService.php
Normal 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
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateKpiTargetsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('kpi_targets', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->integer('target_value');
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Unique constraint untuk mencegah duplikasi target aktif per user (satu target aktif per user)
|
||||||
|
$table->unique(['user_id', 'is_active'], 'kpi_targets_user_active_unique');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('kpi_targets');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateKpiAchievementsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('kpi_achievements', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->foreignId('kpi_target_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->integer('target_value'); // Menyimpan target value secara langsung untuk historical tracking
|
||||||
|
$table->integer('actual_value')->default(0);
|
||||||
|
$table->decimal('achievement_percentage', 5, 2)->default(0);
|
||||||
|
$table->integer('year');
|
||||||
|
$table->integer('month');
|
||||||
|
$table->text('notes')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Unique constraint untuk mencegah duplikasi achievement per user per bulan
|
||||||
|
// Note: Tidak menggunakan kpi_target_id karena target sekarang permanen per user
|
||||||
|
$table->unique(['user_id', 'year', 'month']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('kpi_achievements');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,10 @@ class MenuSeeder extends Seeder
|
|||||||
[
|
[
|
||||||
'name' => 'Histori Stock',
|
'name' => 'Histori Stock',
|
||||||
'link' => 'stock-audit.index'
|
'link' => 'stock-audit.index'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Target',
|
||||||
|
'link' => 'kpi.targets.index'
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,63 +1,67 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
function moneyFormat(n, currency) {
|
function moneyFormat(n, currency) {
|
||||||
n = (n != null) ? n : 0;
|
n = n != null ? n : 0;
|
||||||
var v = parseFloat(n).toFixed(0);
|
var v = parseFloat(n).toFixed(0);
|
||||||
return currency + " " + v.replace(/./g, function (c, i, a) {
|
return (
|
||||||
return i > 0 && c !== "," && (a.length - i) % 3 === 0 ? "." + c : c;
|
currency +
|
||||||
});
|
" " +
|
||||||
|
v.replace(/./g, function (c, i, a) {
|
||||||
|
return i > 0 && c !== "," && (a.length - i) % 3 === 0 ? "." + c : c;
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var KTToastr = function () {
|
var KTToastr = (function () {
|
||||||
var successToastr = function (msg, title) {
|
var successToastr = function (msg, title) {
|
||||||
toastr.options = {
|
toastr.options = {
|
||||||
"closeButton": true,
|
closeButton: true,
|
||||||
"debug": false,
|
debug: false,
|
||||||
"newestOnTop": false,
|
newestOnTop: false,
|
||||||
"progressBar": false,
|
progressBar: false,
|
||||||
"positionClass": "toast-top-right",
|
positionClass: "toast-top-right",
|
||||||
"preventDuplicates": true,
|
preventDuplicates: true,
|
||||||
"onclick": null,
|
onclick: null,
|
||||||
"showDuration": "500",
|
showDuration: "500",
|
||||||
"hideDuration": "500",
|
hideDuration: "500",
|
||||||
"timeOut": "3000",
|
timeOut: "3000",
|
||||||
"extendedTimeOut": "3000",
|
extendedTimeOut: "3000",
|
||||||
"showEasing": "swing",
|
showEasing: "swing",
|
||||||
"hideEasing": "swing",
|
hideEasing: "swing",
|
||||||
"showMethod": "fadeIn",
|
showMethod: "fadeIn",
|
||||||
"hideMethod": "fadeOut"
|
hideMethod: "fadeOut",
|
||||||
};
|
};
|
||||||
var $toast = toastr["success"](msg, title);
|
var $toast = toastr["success"](msg, title);
|
||||||
|
|
||||||
if (typeof $toast === 'undefined') {
|
if (typeof $toast === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
var errorToastr = function (msg, title) {
|
var errorToastr = function (msg, title) {
|
||||||
toastr.options = {
|
toastr.options = {
|
||||||
"closeButton": true,
|
closeButton: true,
|
||||||
"debug": false,
|
debug: false,
|
||||||
"newestOnTop": false,
|
newestOnTop: false,
|
||||||
"progressBar": false,
|
progressBar: false,
|
||||||
"positionClass": "toast-top-right",
|
positionClass: "toast-top-right",
|
||||||
"preventDuplicates": true,
|
preventDuplicates: true,
|
||||||
"onclick": null,
|
onclick: null,
|
||||||
"showDuration": "500",
|
showDuration: "500",
|
||||||
"hideDuration": "500",
|
hideDuration: "500",
|
||||||
"timeOut": "3000",
|
timeOut: "3000",
|
||||||
"extendedTimeOut": "3000",
|
extendedTimeOut: "3000",
|
||||||
"showEasing": "swing",
|
showEasing: "swing",
|
||||||
"hideEasing": "swing",
|
hideEasing: "swing",
|
||||||
"showMethod": "fadeIn",
|
showMethod: "fadeIn",
|
||||||
"hideMethod": "fadeOut"
|
hideMethod: "fadeOut",
|
||||||
};
|
};
|
||||||
var $toast = toastr["error"](msg, title);
|
var $toast = toastr["error"](msg, title);
|
||||||
|
|
||||||
if (typeof $toast === 'undefined') {
|
if (typeof $toast === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initSuccess: function (msg, title) {
|
initSuccess: function (msg, title) {
|
||||||
@@ -65,11 +69,29 @@ var KTToastr = function () {
|
|||||||
},
|
},
|
||||||
initError: function (msg, title) {
|
initError: function (msg, title) {
|
||||||
errorToastr(msg, title);
|
errorToastr(msg, title);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}();
|
})();
|
||||||
|
|
||||||
jQuery(document).ready(function () {
|
jQuery(document).ready(function () {
|
||||||
var li = $('.kt-menu__item--active');
|
var li = $(".kt-menu__item--active");
|
||||||
li.closest('li.kt-menu__item--submenu').addClass('kt-menu__item--open');
|
li.closest("li.kt-menu__item--submenu").addClass("kt-menu__item--open");
|
||||||
});
|
|
||||||
|
// Initialize Select2 globally
|
||||||
|
if (typeof $.fn.select2 !== "undefined") {
|
||||||
|
$(".select2").select2({
|
||||||
|
theme: "bootstrap4",
|
||||||
|
width: "100%",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize DatePicker globally
|
||||||
|
if (typeof $.fn.datepicker !== "undefined") {
|
||||||
|
$(".datepicker").datepicker({
|
||||||
|
format: "yyyy-mm-dd",
|
||||||
|
autoclose: true,
|
||||||
|
todayHighlight: true,
|
||||||
|
orientation: "bottom auto",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
252
resources/views/kpi/targets/create.blade.php
Normal file
252
resources/views/kpi/targets/create.blade.php
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
@extends('layouts.backapp')
|
||||||
|
|
||||||
|
@section('title', 'Tambah Target KPI')
|
||||||
|
|
||||||
|
@section('styles')
|
||||||
|
<style>
|
||||||
|
.select2-container .select2-selection {
|
||||||
|
height: calc(1.5em + 0.75rem + 2px);
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #495057;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container.select2-container--focus .select2-selection {
|
||||||
|
border-color: #80bdff;
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(197, 214, 233, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container .select2-selection--single .select2-selection__rendered {
|
||||||
|
padding-left: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container .select2-selection--single .select2-selection__arrow {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-results__option--highlighted[aria-selected] {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Limit Select2 dropdown height */
|
||||||
|
.select2-results__options {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for Select2 results */
|
||||||
|
.select2-results__option {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-results__option:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Improve Select2 search box */
|
||||||
|
.select2-search--dropdown .select2-search__field {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="kt-content kt-grid__item kt-grid__item--fluid kt-grid kt-grid--hor" id="kt_content">
|
||||||
|
<div class="kt-container kt-container--fluid kt-grid__item kt-grid__item--fluid">
|
||||||
|
<div class="kt-portlet kt-portlet--mobile">
|
||||||
|
<div class="kt-portlet__head kt-portlet__head--lg">
|
||||||
|
<div class="kt-portlet__head-label">
|
||||||
|
<h3 class="kt-portlet__head-title">
|
||||||
|
Tambah Target KPI
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="kt-portlet__head-toolbar">
|
||||||
|
<div class="kt-portlet__head-actions">
|
||||||
|
<a href="{{ route('kpi.targets.index') }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Kembali
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="kt-portlet__body">
|
||||||
|
<form id="kpi-form" method="POST" action="{{ route('kpi.targets.store') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="user_id" class="form-control-label">Mekanik <span class="text-danger">*</span></label>
|
||||||
|
<select name="user_id" id="user_id" class="form-control select2" required>
|
||||||
|
<option value="">Pilih Mekanik</option>
|
||||||
|
@foreach($mechanics as $mechanic)
|
||||||
|
<option value="{{ $mechanic->id }}" {{ old('user_id') == $mechanic->id ? 'selected' : '' }}>
|
||||||
|
{{ $mechanic->name }} ({{ $mechanic->dealer->name ?? 'N/A' }})
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@if($mechanics->isEmpty())
|
||||||
|
<div class="alert alert-warning mt-2">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
Tidak ada mekanik yang ditemukan. Pastikan ada user dengan role "mechanic" di sistem.
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
Ditemukan {{ $mechanics->count() }} mekanik.
|
||||||
|
@if($mechanics->count() >= 50)
|
||||||
|
Menampilkan 50 mekanik pertama. Gunakan pencarian untuk menemukan mekanik tertentu.
|
||||||
|
@endif
|
||||||
|
</small>
|
||||||
|
@endif
|
||||||
|
@error('user_id')
|
||||||
|
<span class="text-danger">{{ $message }}</span>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="target_value" class="form-control-label">Target Nilai <span class="text-danger">*</span></label>
|
||||||
|
<input type="number" name="target_value" id="target_value" class="form-control"
|
||||||
|
value="{{ old('target_value') }}" min="1" required>
|
||||||
|
@error('target_value')
|
||||||
|
<span class="text-danger">{{ $message }}</span>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description" class="form-control-label">Deskripsi</label>
|
||||||
|
<textarea name="description" id="description" class="form-control" rows="3"
|
||||||
|
placeholder="Deskripsi target (opsional)">{{ old('description') }}</textarea>
|
||||||
|
@error('description')
|
||||||
|
<span class="text-danger">{{ $message }}</span>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-control custom-checkbox">
|
||||||
|
<input type="checkbox" name="is_active" id="is_active" class="custom-control-input"
|
||||||
|
value="1" {{ old('is_active', true) ? 'checked' : '' }}>
|
||||||
|
<label class="custom-control-label" for="is_active">
|
||||||
|
Target Aktif
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Simpan Target
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('kpi.targets.index') }}" class="btn btn-secondary">Kembali</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('javascripts')
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Initialize Select2 with fallback
|
||||||
|
try {
|
||||||
|
// Initialize Select2 for mechanics with search limit
|
||||||
|
$('#user_id').select2({
|
||||||
|
theme: 'bootstrap4',
|
||||||
|
width: '100%',
|
||||||
|
placeholder: 'Pilih Mekanik',
|
||||||
|
allowClear: true,
|
||||||
|
minimumInputLength: 1,
|
||||||
|
maximumInputLength: 50,
|
||||||
|
maximumResultsForSearch: 10,
|
||||||
|
language: {
|
||||||
|
inputTooShort: function() {
|
||||||
|
return "Masukkan minimal 1 karakter untuk mencari";
|
||||||
|
},
|
||||||
|
inputTooLong: function() {
|
||||||
|
return "Maksimal 50 karakter";
|
||||||
|
},
|
||||||
|
noResults: function() {
|
||||||
|
return "Tidak ada hasil ditemukan";
|
||||||
|
},
|
||||||
|
searching: function() {
|
||||||
|
return "Mencari...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Select2 not available, using regular select');
|
||||||
|
// Fallback: ensure regular select works
|
||||||
|
$('.select2').removeClass('select2').addClass('form-control');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form validation
|
||||||
|
$('#kpi-form').on('submit', function(e) {
|
||||||
|
var isValid = true;
|
||||||
|
var errors = [];
|
||||||
|
|
||||||
|
// Clear previous errors
|
||||||
|
$('.text-danger').remove();
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!$('#user_id').val()) {
|
||||||
|
errors.push('Mekanik harus dipilih');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$('#target_value').val() || $('#target_value').val() < 1) {
|
||||||
|
errors.push('Target nilai harus diisi dan minimal 1');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (typeof Swal !== 'undefined') {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Validasi Gagal',
|
||||||
|
html: errors.join('<br>'),
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert('Validasi Gagal:\n' + errors.join('\n'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
263
resources/views/kpi/targets/edit.blade.php
Normal file
263
resources/views/kpi/targets/edit.blade.php
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
@extends('layouts.backapp')
|
||||||
|
|
||||||
|
@section('title', 'Edit Target KPI')
|
||||||
|
|
||||||
|
@section('styles')
|
||||||
|
<style>
|
||||||
|
.select2-container .select2-selection {
|
||||||
|
height: calc(1.5em + 0.75rem + 2px);
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #495057;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container.select2-container--focus .select2-selection {
|
||||||
|
border-color: #80bdff;
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container .select2-selection--single .select2-selection__rendered {
|
||||||
|
padding-left: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container .select2-selection--single .select2-selection__arrow {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-results__option--highlighted[aria-selected] {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure Select2 is visible */
|
||||||
|
.select2-container {
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-dropdown {
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix Select2 width */
|
||||||
|
.select2-container--default .select2-selection--single {
|
||||||
|
height: calc(1.5em + 0.75rem + 2px);
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Limit Select2 dropdown height */
|
||||||
|
.select2-results__options {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for Select2 results */
|
||||||
|
.select2-results__option {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-results__option:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Improve Select2 search box */
|
||||||
|
.select2-search--dropdown .select2-search__field {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="kt-content kt-grid__item kt-grid__item--fluid kt-grid kt-grid--hor" id="kt_content">
|
||||||
|
<div class="kt-container kt-container--fluid kt-grid__item kt-grid__item--fluid">
|
||||||
|
<div class="kt-portlet kt-portlet--mobile">
|
||||||
|
<div class="kt-portlet__head kt-portlet__head--lg">
|
||||||
|
<div class="kt-portlet__head-label">
|
||||||
|
<h3 class="kt-portlet__head-title">
|
||||||
|
Edit Target KPI
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="kt-portlet__head-toolbar">
|
||||||
|
<div class="kt-portlet__head-actions">
|
||||||
|
<a href="{{ route('kpi.targets.index') }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Kembali
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="kt-portlet__body">
|
||||||
|
|
||||||
|
<form id="kpi-form" method="POST" action="{{ route('kpi.targets.update', $target->id) }}">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="user_id" class="form-control-label">Mekanik <span class="text-danger">*</span></label>
|
||||||
|
<select name="user_id" id="user_id" class="form-control select2" required>
|
||||||
|
<option value="">Pilih Mekanik</option>
|
||||||
|
@foreach($mechanics as $mechanic)
|
||||||
|
@php
|
||||||
|
$isSelected = old('user_id', $target->user_id) == $mechanic->id;
|
||||||
|
@endphp
|
||||||
|
<option value="{{ $mechanic->id }}"
|
||||||
|
{{ $isSelected ? 'selected' : '' }}>
|
||||||
|
{{ $mechanic->name }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
|
||||||
|
@error('user_id')
|
||||||
|
<span class="text-danger">{{ $message }}</span>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="target_value" class="form-control-label">Target Nilai <span class="text-danger">*</span></label>
|
||||||
|
<input type="number" name="target_value" id="target_value" class="form-control"
|
||||||
|
value="{{ old('target_value', $target->target_value) }}" min="1" required>
|
||||||
|
@error('target_value')
|
||||||
|
<span class="text-danger">{{ $message }}</span>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description" class="form-control-label">Deskripsi</label>
|
||||||
|
<textarea name="description" id="description" class="form-control" rows="3"
|
||||||
|
placeholder="Deskripsi target (opsional)">{{ old('description', $target->description) }}</textarea>
|
||||||
|
@error('description')
|
||||||
|
<span class="text-danger">{{ $message }}</span>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-control custom-checkbox">
|
||||||
|
<input type="checkbox" name="is_active" id="is_active" class="custom-control-input"
|
||||||
|
value="1" {{ old('is_active', $target->is_active) ? 'checked' : '' }}>
|
||||||
|
<label class="custom-control-label" for="is_active">
|
||||||
|
Target Aktif
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Update Target
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('kpi.targets.index') }}" class="btn btn-secondary">Kembali</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('javascripts')
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Initialize Select2 with fallback and delay
|
||||||
|
setTimeout(function() {
|
||||||
|
try {
|
||||||
|
// Initialize Select2 for mechanics with search limit
|
||||||
|
$('#user_id').select2({
|
||||||
|
theme: 'bootstrap4',
|
||||||
|
width: '100%',
|
||||||
|
placeholder: 'Pilih Mekanik',
|
||||||
|
allowClear: true,
|
||||||
|
minimumInputLength: 1,
|
||||||
|
maximumInputLength: 50,
|
||||||
|
maximumResultsForSearch: 10,
|
||||||
|
language: {
|
||||||
|
inputTooShort: function() {
|
||||||
|
return "Masukkan minimal 1 karakter untuk mencari";
|
||||||
|
},
|
||||||
|
inputTooLong: function() {
|
||||||
|
return "Maksimal 50 karakter";
|
||||||
|
},
|
||||||
|
noResults: function() {
|
||||||
|
return "Tidak ada hasil ditemukan";
|
||||||
|
},
|
||||||
|
searching: function() {
|
||||||
|
return "Mencari...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Select2 not available, using regular select');
|
||||||
|
// Fallback: ensure regular select works
|
||||||
|
$('.select2').removeClass('select2').addClass('form-control');
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Form validation
|
||||||
|
$('#kpi-form').on('submit', function(e) {
|
||||||
|
var isValid = true;
|
||||||
|
var errors = [];
|
||||||
|
|
||||||
|
// Clear previous errors
|
||||||
|
$('.text-danger').remove();
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!$('#user_id').val()) {
|
||||||
|
errors.push('Mekanik harus dipilih');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$('#target_value').val() || $('#target_value').val() < 1) {
|
||||||
|
errors.push('Target nilai harus diisi dan minimal 1');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (typeof Swal !== 'undefined') {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Validasi Gagal',
|
||||||
|
html: errors.join('<br>'),
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert('Validasi Gagal:\n' + errors.join('\n'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
212
resources/views/kpi/targets/index.blade.php
Normal file
212
resources/views/kpi/targets/index.blade.php
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
@extends('layouts.backapp')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="kt-content kt-grid__item kt-grid__item--fluid kt-grid kt-grid--hor" id="kt_content">
|
||||||
|
<div class="kt-container kt-container--fluid kt-grid__item kt-grid__item--fluid">
|
||||||
|
<div class="kt-portlet kt-portlet--mobile">
|
||||||
|
<div class="kt-portlet__head kt-portlet__head--lg">
|
||||||
|
<div class="kt-portlet__head-label">
|
||||||
|
<h3 class="kt-portlet__head-title">
|
||||||
|
Manajemen Target KPI
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="kt-portlet__head-toolbar">
|
||||||
|
<div class="kt-portlet__head-actions">
|
||||||
|
<a href="{{ route('kpi.targets.create') }}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Tambah Target
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="kt-portlet__body">
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
{{ session('success') }}
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(session('error'))
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
{{ session('error') }}
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-bordered" id="kpiTargetsTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>No</th>
|
||||||
|
<th>Mekanik</th>
|
||||||
|
<th>Target</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($targets as $target)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $loop->iteration }}</td>
|
||||||
|
<td>{{ $target->user->name }}</td>
|
||||||
|
<td>{{ number_format($target->target_value) }}</td>
|
||||||
|
<td>
|
||||||
|
@if($target->is_active)
|
||||||
|
<span class="badge badge-success">Aktif</span>
|
||||||
|
@else
|
||||||
|
<span class="badge badge-secondary">Nonaktif</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="{{ route('kpi.targets.show', $target->id) }}"
|
||||||
|
class="btn btn-sm btn-info" title="Detail">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('kpi.targets.edit', $target->id) }}"
|
||||||
|
class="btn btn-sm btn-warning" title="Edit">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-{{ $target->is_active ? 'warning' : 'success' }}"
|
||||||
|
onclick="toggleStatus({{ $target->id }})"
|
||||||
|
title="{{ $target->is_active ? 'Nonaktifkan' : 'Aktifkan' }}">
|
||||||
|
<i class="fas fa-{{ $target->is_active ? 'pause' : 'play' }}"></i>
|
||||||
|
</button>
|
||||||
|
<form action="{{ route('kpi.targets.destroy', $target->id) }}"
|
||||||
|
method="POST"
|
||||||
|
style="display: inline;"
|
||||||
|
onsubmit="return confirm('Yakin ingin menghapus target ini?')">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger" title="Hapus">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center">Tidak ada data target KPI</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($targets->hasPages())
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
{{ $targets->links() }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Modal -->
|
||||||
|
<div class="modal fade" id="filterModal" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Filter Target KPI</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal">
|
||||||
|
<span>×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form action="{{ route('kpi.targets.index') }}" method="GET">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Mekanik</label>
|
||||||
|
<select name="user_id" class="form-control">
|
||||||
|
<option value="">Semua Mekanik</option>
|
||||||
|
@foreach($mechanics as $mechanic)
|
||||||
|
<option value="{{ $mechanic->id }}"
|
||||||
|
{{ request('user_id') == $mechanic->id ? 'selected' : '' }}>
|
||||||
|
{{ $mechanic->name }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Status</label>
|
||||||
|
<select name="is_active" class="form-control">
|
||||||
|
<option value="">Semua Status</option>
|
||||||
|
<option value="1" {{ request('is_active') == '1' ? 'selected' : '' }}>Aktif</option>
|
||||||
|
<option value="0" {{ request('is_active') == '0' ? 'selected' : '' }}>Nonaktif</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Filter</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('javascripts')
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Initialize DataTable
|
||||||
|
$('#kpiTargetsTable').DataTable({
|
||||||
|
"pageLength": 25,
|
||||||
|
"order": [[0, "asc"]],
|
||||||
|
"language": {
|
||||||
|
"url": "//cdn.datatables.net/plug-ins/1.10.24/i18n/Indonesian.json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto hide alerts after 5 seconds
|
||||||
|
setTimeout(function() {
|
||||||
|
$('.alert').fadeOut('slow');
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleStatus(targetId) {
|
||||||
|
if (confirm('Yakin ingin mengubah status target ini?')) {
|
||||||
|
$.ajax({
|
||||||
|
url: '{{ route("kpi.targets.toggle-status", ":id") }}'.replace(':id', targetId),
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
_token: '{{ csrf_token() }}'
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Berhasil',
|
||||||
|
text: response.message,
|
||||||
|
timer: 2000,
|
||||||
|
showConfirmButton: false
|
||||||
|
}).then(function() {
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Error',
|
||||||
|
text: response.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Error',
|
||||||
|
text: 'Terjadi kesalahan saat mengubah status'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
193
resources/views/kpi/targets/show.blade.php
Normal file
193
resources/views/kpi/targets/show.blade.php
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
@extends('layouts.backapp')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="kt-content kt-grid__item kt-grid__item--fluid kt-grid kt-grid--hor" id="kt_content">
|
||||||
|
<div class="kt-container kt-container--fluid kt-grid__item kt-grid__item--fluid">
|
||||||
|
<div class="kt-portlet kt-portlet--mobile">
|
||||||
|
<div class="kt-portlet__head kt-portlet__head--lg">
|
||||||
|
<div class="kt-portlet__head-label">
|
||||||
|
<h3 class="kt-portlet__head-title">
|
||||||
|
Detail Target KPI
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="kt-portlet__head-toolbar">
|
||||||
|
<div class="kt-portlet__head-actions">
|
||||||
|
<a href="{{ route('kpi.targets.edit', $target->id) }}" class="btn btn-warning">
|
||||||
|
<i class="fas fa-edit"></i> Edit
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('kpi.targets.index') }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Kembali
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="kt-portlet__body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<table class="table table-borderless">
|
||||||
|
<tr>
|
||||||
|
<td width="150"><strong>Mekanik</strong></td>
|
||||||
|
<td>: {{ $target->user->name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Email</strong></td>
|
||||||
|
<td>: {{ $target->user->email }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Dealer</strong></td>
|
||||||
|
<td>: {{ $target->user->dealer->name ?? 'N/A' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Target Nilai</strong></td>
|
||||||
|
<td>: {{ number_format($target->target_value) }} Pekerjaan</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Status</strong></td>
|
||||||
|
<td>:
|
||||||
|
@if($target->is_active)
|
||||||
|
<span class="badge badge-success">Aktif</span>
|
||||||
|
@else
|
||||||
|
<span class="badge badge-secondary">Nonaktif</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<table class="table table-borderless">
|
||||||
|
<tr>
|
||||||
|
<td width="150"><strong>Jenis Target</strong></td>
|
||||||
|
<td>: <span class="badge badge-info">Target Permanen</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Berlaku Sejak</strong></td>
|
||||||
|
<td>: {{ $target->created_at->format('d/m/Y') }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Dibuat</strong></td>
|
||||||
|
<td>: {{ $target->created_at->format('d/m/Y H:i') }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Terakhir Update</strong></td>
|
||||||
|
<td>: {{ $target->updated_at->format('d/m/Y H:i') }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Total Pencapaian</strong></td>
|
||||||
|
<td>: {{ $target->achievements->count() }} Bulan</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($target->description)
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<h6><strong>Deskripsi:</strong></h6>
|
||||||
|
<p class="text-muted">{{ $target->description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<!-- Achievement History -->
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<h5><i class="fas fa-chart-line"></i> Riwayat Pencapaian Bulanan</h5>
|
||||||
|
@if($target->achievements->count() > 0)
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Periode</th>
|
||||||
|
<th>Target</th>
|
||||||
|
<th>Aktual</th>
|
||||||
|
<th>Pencapaian</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($target->achievements->sortByDesc('year')->sortByDesc('month') as $achievement)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $achievement->getPeriodDisplayName() }}</td>
|
||||||
|
<td>{{ number_format($achievement->target_value) }}</td>
|
||||||
|
<td>{{ number_format($achievement->actual_value) }}</td>
|
||||||
|
<td>{{ number_format($achievement->achievement_percentage, 1) }}%</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge badge-{{ $achievement->status_color }}">
|
||||||
|
@switch($achievement->status)
|
||||||
|
@case('exceeded')
|
||||||
|
Melebihi Target
|
||||||
|
@break
|
||||||
|
@case('good')
|
||||||
|
Baik
|
||||||
|
@break
|
||||||
|
@case('fair')
|
||||||
|
Cukup
|
||||||
|
@break
|
||||||
|
@case('poor')
|
||||||
|
Kurang
|
||||||
|
@break
|
||||||
|
@default
|
||||||
|
Tidak Diketahui
|
||||||
|
@endswitch
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fas fa-info-circle"></i> Belum ada data pencapaian untuk target ini.
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Summary Statistics -->
|
||||||
|
@if($target->achievements->count() > 0)
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<h5><i class="fas fa-chart-bar"></i> Statistik Pencapaian</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card bg-primary text-white">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h4>{{ $target->achievements->count() }}</h4>
|
||||||
|
<small>Total Pencapaian</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card bg-success text-white">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h4>{{ $target->achievements->where('achievement_percentage', '>=', 100)->count() }}</h4>
|
||||||
|
<small>Target Tercapai</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card bg-info text-white">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h4>{{ number_format($target->achievements->avg('achievement_percentage'), 1) }}%</h4>
|
||||||
|
<small>Rata-rata Pencapaian</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card bg-warning text-white">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h4>{{ number_format($target->achievements->max('achievement_percentage'), 1) }}%</h4>
|
||||||
|
<small>Pencapaian Tertinggi</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -222,6 +222,22 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@endcan
|
@endcan
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<span>KPI</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@can('view', $menus['kpi.targets.index'])
|
||||||
|
<li class="kt-menu__item" aria-haspopup="true">
|
||||||
|
<a href="{{ route('kpi.targets.index') }}" class="kt-menu__link">
|
||||||
|
<i class="fa fa-user-cog" style="display: flex; align-items: center; margin-right: 10px;"></i>
|
||||||
|
<span class="kt-menu__link-text">Target</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
@endcan
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -320,6 +320,8 @@ use Illuminate\Support\Facades\Auth;
|
|||||||
border-color: #28a745;
|
border-color: #28a745;
|
||||||
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
|
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@@ -359,6 +361,58 @@ use Illuminate\Support\Facades\Auth;
|
|||||||
<b>Dealer {{ $mechanic->dealer_name }}</b><br><br>
|
<b>Dealer {{ $mechanic->dealer_name }}</b><br><br>
|
||||||
<a href="#">Total {{ $count_transaction_dealers }} Pekerjaan terkirim pada dealer</a><br>
|
<a href="#">Total {{ $count_transaction_dealers }} Pekerjaan terkirim pada dealer</a><br>
|
||||||
<a href="#">Anda telah posting {{ $count_transaction_users }} pekerjaan</a>
|
<a href="#">Anda telah posting {{ $count_transaction_users }} pekerjaan</a>
|
||||||
|
|
||||||
|
<!-- KPI Information -->
|
||||||
|
<div class="mt-3">
|
||||||
|
@if($kpiData['has_target'])
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<small class="text-muted">Target {{ $kpiData['period'] }}</small><br>
|
||||||
|
<strong class="text-primary">{{ number_format($kpiData['target']) }} Pekerjaan</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<small class="text-muted">Pencapaian</small><br>
|
||||||
|
<strong class="text-{{ $kpiData['status_color'] }}">{{ number_format($kpiData['actual']) }} Pekerjaan</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<small class="text-muted">Progress</small>
|
||||||
|
<small class="text-{{ $kpiData['status_color'] }} font-weight-bold">{{ $kpiData['percentage'] }}%</small>
|
||||||
|
</div>
|
||||||
|
<div class="progress" style="height: 8px;">
|
||||||
|
@php
|
||||||
|
$progressWidth = min(100, $kpiData['percentage']);
|
||||||
|
@endphp
|
||||||
|
<div class="progress-bar bg-{{ $kpiData['status_color'] }}"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: {{ $progressWidth }}%"
|
||||||
|
aria-valuenow="{{ $kpiData['percentage'] }}"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1">
|
||||||
|
@if($kpiData['status'] == 'exceeded')
|
||||||
|
<small class="text-success"><i class="fa fa-check-circle"></i> Target tercapai!</small>
|
||||||
|
@elseif($kpiData['status'] == 'good')
|
||||||
|
<small class="text-info"><i class="fa fa-arrow-up"></i> Performa baik</small>
|
||||||
|
@elseif($kpiData['status'] == 'fair')
|
||||||
|
<small class="text-warning"><i class="fa fa-exclamation-triangle"></i> Perlu peningkatan</small>
|
||||||
|
@elseif($kpiData['status'] == 'poor')
|
||||||
|
<small class="text-danger"><i class="fa fa-times-circle"></i> Perlu perbaikan</small>
|
||||||
|
@else
|
||||||
|
<small class="text-secondary"><i class="fa fa-clock"></i> Belum ada data</small>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="text-center py-2">
|
||||||
|
<i class="fa fa-info-circle text-muted"></i>
|
||||||
|
<small class="text-muted">Belum ada target KPI untuk {{ $kpiData['period'] }}</small>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<div class="text-center mt-2">
|
<div class="text-center mt-2">
|
||||||
@@ -1665,6 +1719,8 @@ use Illuminate\Support\Facades\Auth;
|
|||||||
$(this).val('0.00');
|
$(this).val('0.00');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle when input loses focus - set default if empty
|
// Handle when input loses focus - set default if empty
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use App\Http\Controllers\WarehouseManagement\ProductsController;
|
|||||||
use App\Http\Controllers\WorkController;
|
use App\Http\Controllers\WorkController;
|
||||||
use App\Http\Controllers\WarehouseManagement\MutationsController;
|
use App\Http\Controllers\WarehouseManagement\MutationsController;
|
||||||
use App\Http\Controllers\WarehouseManagement\StockAuditController;
|
use App\Http\Controllers\WarehouseManagement\StockAuditController;
|
||||||
|
use App\Http\Controllers\KPI\TargetsController;
|
||||||
use App\Models\Menu;
|
use App\Models\Menu;
|
||||||
use App\Models\Privilege;
|
use App\Models\Privilege;
|
||||||
use App\Models\Role;
|
use App\Models\Role;
|
||||||
@@ -282,6 +283,14 @@ Route::group(['middleware' => 'auth'], function() {
|
|||||||
Route::get('{stockLog}/detail', 'getDetail')->name('detail');
|
Route::get('{stockLog}/detail', 'getDetail')->name('detail');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// KPI Routes for Admins
|
||||||
|
Route::prefix('kpi')->middleware(['adminRole'])->group(function () {
|
||||||
|
// Target Management
|
||||||
|
Route::resource('targets', TargetsController::class, ['as' => 'kpi']);
|
||||||
|
Route::post('/targets/{target}/toggle-status', [TargetsController::class, 'toggleStatus'])->name('kpi.targets.toggle-status');
|
||||||
|
Route::get('/targets/user/{user}', [TargetsController::class, 'getUserTargets'])->name('kpi.targets.user');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Auth::routes();
|
Auth::routes();
|
||||||
|
|||||||
Reference in New Issue
Block a user