828 lines
35 KiB
PHP
828 lines
35 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use Illuminate\Http\Request;
|
|
use App\Models\Staff;
|
|
use App\Models\TrainerCommission;
|
|
use App\Models\SalaryAdvanceDeduction;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Facades\Auth;
|
|
|
|
class StaffController extends Controller
|
|
{
|
|
public function index(Request $request)
|
|
{
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
$query = Staff::with(['branch', 'documents']);
|
|
|
|
if ($user && $user->isReceptionist()) {
|
|
$query->where('branch_id', $user->branch_id);
|
|
} else if ($branchId = $request->query('branch_id')) {
|
|
$query->where('branch_id', $branchId);
|
|
}
|
|
|
|
return response()->json($query->get());
|
|
}
|
|
|
|
public function show($id)
|
|
{
|
|
$staff = Staff::with(['branch', 'documents'])->findOrFail($id);
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
if ($user && $user->isReceptionist() && $staff->branch_id != $user->branch_id) {
|
|
return response()->json(['message' => 'Unauthorized'], 403);
|
|
}
|
|
return response()->json($staff);
|
|
}
|
|
|
|
public function store(Request $request)
|
|
{
|
|
Log::info('Creating standalone staff record', $request->all());
|
|
|
|
$validated = $request->validate([
|
|
'full_name' => 'required|string|max:255',
|
|
'email' => 'required|string|email|max:255|unique:staff,email',
|
|
'phone' => ['required', 'string', 'max:20', 'regex:/^(\+91|91|0)?[6-9]\d{9}$|^(\+971|971|0)?5[024568]\d{7}$/'],
|
|
'role' => 'required|string',
|
|
'branch_id' => 'required|exists:branches,id',
|
|
'joining_date' => 'required|date',
|
|
'status' => 'required|string',
|
|
'salary_type' => 'required|string',
|
|
'salary_amount' => 'required|numeric',
|
|
'advance_enabled' => 'nullable|boolean',
|
|
'advance_amount' => 'nullable|numeric',
|
|
'advance_repayment_mode' => 'nullable|string',
|
|
'advance_months' => 'nullable|integer',
|
|
'commission_enabled' => 'nullable|boolean',
|
|
'commission_amount' => 'nullable|numeric',
|
|
'commission_member_count' => 'nullable|integer',
|
|
'emirates_id' => 'nullable|string|max:255',
|
|
'emirates_id_expiry' => 'nullable|date',
|
|
'family_member_name' => 'nullable|string|max:255',
|
|
'family_member_relation' => 'nullable|string|max:255',
|
|
'family_member_contact' => ['nullable', 'string', 'max:255', 'regex:/^(\+91|91|0)?[6-9]\d{9}$|^(\+971|971|0)?5[024568]\d{7}$/'],
|
|
'salary_reminder_enabled' => 'nullable|boolean',
|
|
'document_expiry_enabled' => 'nullable|boolean',
|
|
'family_members' => 'nullable|array',
|
|
'family_members.*.name' => 'required|string|max:255',
|
|
'family_members.*.relation' => 'required|string|max:255',
|
|
'family_members.*.contact' => ['required', 'string', 'max:255', 'regex:/^(\+91|91|0)?[6-9]\d{9}$|^(\+971|971|0)?5[024568]\d{7}$/'],
|
|
'documents' => 'nullable|array',
|
|
'documents.*.name' => 'required|string|max:255',
|
|
'documents.*.document_number' => 'nullable|string|max:255',
|
|
'documents.*.expiry_date' => 'nullable|date',
|
|
'documents.*.reminder_days' => 'nullable|integer',
|
|
]);
|
|
|
|
// Branch Start Date Validation - REMOVED AS PER USER REQUEST
|
|
/*
|
|
$branch = \App\Models\Branch::find($validated['branch_id']);
|
|
if ($branch && $validated['joining_date'] < $branch->operational_start_date) {
|
|
return response()->json([
|
|
'errors' => ['joining_date' => ["Joining date cannot be before branch start date ({$branch->operational_start_date})"]]
|
|
], 422);
|
|
}
|
|
*/
|
|
|
|
if (empty($validated['staff_id_code'])) {
|
|
$role = strtoupper($validated['role'] ?? 'STAFF');
|
|
$count = Staff::count() + 1;
|
|
$validated['staff_id_code'] = "USR-{$role}-" . str_pad($count, 3, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
$status = $validated['status'] ?? 'Active';
|
|
$validated['status'] = $status;
|
|
|
|
$staff = Staff::create($validated);
|
|
|
|
// Handle Salary Advance Deductions
|
|
if ($staff->advance_enabled) {
|
|
if ($staff->advance_repayment_mode === 'Divide by Months') {
|
|
$months = $staff->advance_months ?: 1;
|
|
$monthlyDeduction = $staff->advance_amount / $months;
|
|
|
|
SalaryAdvanceDeduction::create([
|
|
'staff_id' => $staff->id,
|
|
'advance_amount' => $staff->advance_amount,
|
|
'total_months' => $months,
|
|
'monthly_deduction' => $monthlyDeduction,
|
|
'remaining_amount' => $staff->advance_amount,
|
|
'paid_amount' => 0,
|
|
'status' => 'Pending'
|
|
]);
|
|
}
|
|
|
|
// Record immediate account debit and expense for the advance
|
|
$this->recordFinancialsForAdvance($staff, $staff->advance_amount);
|
|
}
|
|
|
|
// Handle Documents
|
|
if ($request->has('documents')) {
|
|
foreach ($request->input('documents') as $index => $doc) {
|
|
$path = null;
|
|
if ($request->hasFile("documents.{$index}.file")) {
|
|
$path = $request->file("documents.{$index}.file")->store('staff_documents', 'public');
|
|
}
|
|
|
|
$staff->documents()->create([
|
|
'name' => $doc['name'],
|
|
'document_number' => $doc['document_number'] ?? null,
|
|
'expiry_date' => $doc['expiry_date'] ?? null,
|
|
'reminder_days' => $doc['reminder_days'] ?? 30,
|
|
'path' => $path,
|
|
]);
|
|
}
|
|
}
|
|
|
|
return response()->json(['message' => 'Staff created successfully', 'staff' => $staff], 201);
|
|
}
|
|
|
|
public function update(Request $request, $id)
|
|
{
|
|
$staff = Staff::findOrFail($id);
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
if ($user && $user->isReceptionist() && $staff->branch_id != $user->branch_id) {
|
|
return response()->json(['message' => 'Unauthorized'], 403);
|
|
}
|
|
|
|
Log::info('Updating standalone staff record', $request->all());
|
|
|
|
$validated = $request->validate([
|
|
'full_name' => 'nullable|string|max:255',
|
|
'email' => 'nullable|string|max:255',
|
|
'phone' => ['nullable', 'string', 'max:20', 'regex:/^(\+91|91|0)?[6-9]\d{9}$|^(\+971|971|0)?5[024568]\d{7}$/'],
|
|
'designation' => 'nullable|string|max:255',
|
|
'role' => 'nullable|string',
|
|
'branch_id' => 'nullable|exists:branches,id',
|
|
'joining_date' => 'nullable|date',
|
|
'status' => 'nullable|string',
|
|
'salary_type' => 'nullable|string',
|
|
'salary_amount' => 'nullable|numeric',
|
|
'salary_cycle' => 'nullable|string',
|
|
'advance_enabled' => 'nullable|boolean',
|
|
'advance_amount' => 'nullable|numeric',
|
|
'advance_repayment_mode' => 'nullable|string',
|
|
'advance_months' => 'nullable|integer',
|
|
'commission_enabled' => 'nullable|boolean',
|
|
'commission_amount' => 'nullable|numeric',
|
|
'commission_member_count' => 'nullable|integer',
|
|
'apply_from' => 'nullable|string',
|
|
'emirates_id' => 'nullable|string|max:255',
|
|
'emirates_id_expiry' => 'nullable|date',
|
|
'family_member_name' => 'nullable|string|max:255',
|
|
'family_member_relation' => 'nullable|string|max:255',
|
|
'family_member_contact' => ['nullable', 'string', 'max:255', 'regex:/^(\+91|91|0)?[6-9]\d{9}$|^(\+971|971|0)?5[024568]\d{7}$/'],
|
|
'salary_reminder_enabled' => 'nullable|boolean',
|
|
'document_expiry_enabled' => 'nullable|boolean',
|
|
'family_members' => 'nullable|array',
|
|
'family_members.*.name' => 'required|string|max:255',
|
|
'family_members.*.relation' => 'required|string|max:255',
|
|
'family_members.*.contact' => ['required', 'string', 'max:255', 'regex:/^(\+91|91|0)?[6-9]\d{9}$|^(\+971|971|0)?5[024568]\d{7}$/'],
|
|
'documents' => 'nullable|array',
|
|
'documents.*.id' => 'nullable|exists:staff_documents,id',
|
|
'documents.*.name' => 'required|string|max:255',
|
|
'documents.*.document_number' => 'nullable|string|max:255',
|
|
'documents.*.expiry_date' => 'nullable|date',
|
|
'documents.*.reminder_days' => 'nullable|integer',
|
|
]);
|
|
|
|
$status = $validated['status'] ?? 'Active';
|
|
$validated['status'] = $status;
|
|
|
|
$staff->update($validated);
|
|
|
|
// Handle Documents Update
|
|
if ($request->has('documents')) {
|
|
$existingDocIds = [];
|
|
foreach ($request->input('documents') as $index => $doc) {
|
|
$path = null;
|
|
if ($request->hasFile("documents.{$index}.file")) {
|
|
$path = $request->file("documents.{$index}.file")->store('staff_documents', 'public');
|
|
}
|
|
|
|
if (isset($doc['id'])) {
|
|
$staffDoc = \App\Models\StaffDocument::find($doc['id']);
|
|
if ($staffDoc && $staffDoc->staff_id == $staff->id) {
|
|
$updateData = [
|
|
'name' => $doc['name'],
|
|
'document_number' => $doc['document_number'] ?? null,
|
|
'expiry_date' => $doc['expiry_date'] ?? null,
|
|
'reminder_days' => $doc['reminder_days'] ?? 30,
|
|
];
|
|
if ($path) $updateData['path'] = $path;
|
|
$staffDoc->update($updateData);
|
|
$existingDocIds[] = $staffDoc->id;
|
|
}
|
|
} else {
|
|
$newDoc = $staff->documents()->create([
|
|
'name' => $doc['name'],
|
|
'document_number' => $doc['document_number'] ?? null,
|
|
'expiry_date' => $doc['expiry_date'] ?? null,
|
|
'reminder_days' => $doc['reminder_days'] ?? 30,
|
|
'path' => $path,
|
|
]);
|
|
$existingDocIds[] = $newDoc->id;
|
|
}
|
|
}
|
|
// Delete removed documents
|
|
$staff->documents()->whereNotIn('id', $existingDocIds)->get()->each(function($doc) {
|
|
if ($doc->path) Storage::disk('public')->delete($doc->path);
|
|
$doc->delete();
|
|
});
|
|
}
|
|
|
|
// Handle Salary Advance Deductions
|
|
if ($staff->advance_enabled) {
|
|
// Only record financials if advance amount has increased
|
|
if ($request->has('advance_amount')) {
|
|
$oldAdvance = $staff->getOriginal('advance_amount') ?: 0;
|
|
$newAdvance = $staff->advance_amount;
|
|
|
|
if ($newAdvance > $oldAdvance) {
|
|
$additionalAmount = $newAdvance - $oldAdvance;
|
|
$this->recordFinancialsForAdvance($staff, $additionalAmount);
|
|
}
|
|
}
|
|
|
|
if ($staff->advance_repayment_mode === 'Divide by Months') {
|
|
$existing = SalaryAdvanceDeduction::where('staff_id', $staff->id)->where('status', 'Pending')->first();
|
|
$months = $staff->advance_months ?: 1;
|
|
$monthlyDeduction = $staff->advance_amount / $months;
|
|
|
|
if ($existing) {
|
|
$existing->update([
|
|
'advance_amount' => $staff->advance_amount,
|
|
'total_months' => $months,
|
|
'monthly_deduction' => $monthlyDeduction,
|
|
'remaining_amount' => $staff->advance_amount,
|
|
]);
|
|
} else {
|
|
SalaryAdvanceDeduction::create([
|
|
'staff_id' => $staff->id,
|
|
'advance_amount' => $staff->advance_amount,
|
|
'total_months' => $months,
|
|
'monthly_deduction' => $monthlyDeduction,
|
|
'remaining_amount' => $staff->advance_amount,
|
|
'paid_amount' => 0,
|
|
'status' => 'Pending'
|
|
]);
|
|
}
|
|
}
|
|
} else {
|
|
// If advance was enabled but now disabled, mark active deduction as Closed
|
|
if ($staff->getOriginal('advance_enabled')) {
|
|
SalaryAdvanceDeduction::where('staff_id', $staff->id)
|
|
->where('status', 'Pending')
|
|
->update(['status' => 'Closed']);
|
|
}
|
|
}
|
|
|
|
// Handle Trainer Commission History
|
|
if ($staff->commission_enabled && $request->has('commission_member_count')) {
|
|
$effectiveMonth = $request->input('apply_from') === 'next_month'
|
|
? Carbon::now()->addMonth()->format('Y-m')
|
|
: Carbon::now()->format('Y-m');
|
|
|
|
TrainerCommission::updateOrCreate(
|
|
[
|
|
'staff_id' => $staff->id,
|
|
'effective_month' => $effectiveMonth
|
|
],
|
|
[
|
|
'member_count' => $request->input('commission_member_count'),
|
|
'amount_per_head' => $request->input('commission_amount'),
|
|
'total_amount' => $request->input('commission_member_count') * $request->input('commission_amount')
|
|
]
|
|
);
|
|
}
|
|
|
|
return response()->json(['message' => 'Staff updated successfully', 'staff' => $staff]);
|
|
}
|
|
|
|
public function destroy($id)
|
|
{
|
|
$staff = Staff::findOrFail($id);
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
if ($user && $user->isReceptionist() && $staff->branch_id != $user->branch_id) {
|
|
return response()->json(['message' => 'Unauthorized'], 403);
|
|
}
|
|
$staff->delete();
|
|
|
|
return response()->json(['message' => 'Staff deleted successfully']);
|
|
}
|
|
|
|
public function getPayments($id)
|
|
{
|
|
$staff = Staff::findOrFail($id);
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
if ($user && $user->isReceptionist() && $staff->branch_id != $user->branch_id) {
|
|
return response()->json(['message' => 'Unauthorized'], 403);
|
|
}
|
|
$payments = \App\Models\StaffPayment::where('staff_id', $id)->orderBy('payment_date', 'desc')->get();
|
|
return response()->json($payments);
|
|
}
|
|
|
|
public function storePayment(Request $request, $id)
|
|
{
|
|
$staff = Staff::findOrFail($id);
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
if ($user && $user->isReceptionist() && $staff->branch_id != $user->branch_id) {
|
|
return response()->json(['message' => 'Unauthorized'], 403);
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'amount' => 'required|numeric',
|
|
'payment_date' => 'required|date',
|
|
'payment_type' => 'required|string',
|
|
'status' => 'required|string',
|
|
'remarks' => 'nullable|string'
|
|
]);
|
|
|
|
$payment = \App\Models\StaffPayment::create(array_merge($validated, ['staff_id' => $id]));
|
|
|
|
// Log to Accounts as Debit
|
|
\App\Models\Account::create([
|
|
'date' => $validated['payment_date'],
|
|
'time' => \Carbon\Carbon::now()->toTimeString(),
|
|
'credit' => 0,
|
|
'debit' => $validated['amount'],
|
|
'type' => 'salary',
|
|
'accountable_id' => $payment->id,
|
|
'accountable_type' => \App\Models\StaffPayment::class,
|
|
'description' => "{$validated['payment_type']} for {$staff->full_name}"
|
|
]);
|
|
|
|
return response()->json($payment, 201);
|
|
}
|
|
|
|
public function getSettlementDetails(Request $request, $id)
|
|
{
|
|
$staff = Staff::with('branch')->findOrFail($id);
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
if ($user && $user->isReceptionist() && $staff->branch_id != $user->branch_id) {
|
|
return response()->json(['message' => 'Unauthorized'], 403);
|
|
}
|
|
$targetMonthKey = $request->query('month');
|
|
$branch = $staff->branch;
|
|
|
|
if ($targetMonthKey) {
|
|
$nextSettlementMonth = Carbon::parse($targetMonthKey . '-01');
|
|
|
|
// Verify if already paid
|
|
$existing = \App\Models\StaffPayment::where('staff_id', $id)
|
|
->where('payment_type', 'Salary Settlement')
|
|
->where('settlement_month', $targetMonthKey)
|
|
->first();
|
|
|
|
if ($existing) {
|
|
return response()->json([
|
|
'can_settle' => false,
|
|
'message' => "Salary for {$nextSettlementMonth->format('F Y')} is already settled.",
|
|
]);
|
|
}
|
|
} else {
|
|
// Find the next unpaid month logically
|
|
$lastPayment = \App\Models\StaffPayment::where('staff_id', $id)
|
|
->where('payment_type', 'Salary Settlement')
|
|
->orderBy('settlement_month', 'desc')
|
|
->first();
|
|
|
|
$joiningDate = Carbon::parse($staff->joining_date);
|
|
$currentMonth = Carbon::now()->startOfMonth();
|
|
|
|
if ($lastPayment) {
|
|
$nextSettlementMonth = Carbon::parse($lastPayment->settlement_month . '-01')->addMonth();
|
|
} else {
|
|
$nextSettlementMonth = $joiningDate->copy()->startOfMonth();
|
|
}
|
|
|
|
// Only allow settlement if the generation date for the month has arrived
|
|
$genDay = $branch->salary_generation_day ?? 2;
|
|
$generationDate = $nextSettlementMonth->copy()->addMonth()->day(min($genDay, $nextSettlementMonth->copy()->addMonth()->daysInMonth));
|
|
|
|
if (Carbon::now()->lessThan($generationDate)) {
|
|
return response()->json([
|
|
'can_settle' => false,
|
|
'message' => "Salary for {$nextSettlementMonth->format('F Y')} will be available for settlement on " . $generationDate->format('jS F Y'),
|
|
]);
|
|
}
|
|
}
|
|
|
|
// Calculate Cycle and Pro-rata
|
|
$monthKey = $nextSettlementMonth->format('Y-m');
|
|
[$start, $end] = $this->getSalaryCycleRange($monthKey, $branch);
|
|
|
|
$baseSalary = $staff->salary_amount ?: 0;
|
|
$proratedBase = $this->calculateProRatedSalary($baseSalary, $start, $end, $staff->joining_date);
|
|
|
|
$commissionCount = $staff->commission_member_count ?: 0;
|
|
$commissionAmount = $staff->commission_amount ?: 0;
|
|
$totalCommission = $commissionCount * $commissionAmount;
|
|
|
|
// Commission pro-rata logic (optional? User didn't specify, but usually commission is per head so no pro-rata)
|
|
|
|
$advanceDeduction = 0;
|
|
$remainingAdvance = 0;
|
|
|
|
// Find any pending deduction regardless of staff->advance_enabled
|
|
// This ensures repayments continue even if the feature toggle is flipped
|
|
$deduction = SalaryAdvanceDeduction::where('staff_id', $id)
|
|
->where('status', 'Pending')
|
|
->first();
|
|
|
|
if ($deduction) {
|
|
if ($staff->advance_repayment_mode === 'Full Next Month') {
|
|
$advanceDeduction = $deduction->remaining_amount;
|
|
$remainingAdvance = 0;
|
|
} else {
|
|
$advanceDeduction = min($deduction->monthly_deduction, $deduction->remaining_amount);
|
|
$remainingAdvance = $deduction->remaining_amount - $advanceDeduction;
|
|
}
|
|
}
|
|
|
|
$netPayable = $proratedBase + $totalCommission - $advanceDeduction;
|
|
|
|
return response()->json([
|
|
'can_settle' => true,
|
|
'staff_name' => $staff->full_name,
|
|
'period' => $nextSettlementMonth->format('F Y'),
|
|
'settlement_month' => $monthKey,
|
|
'cycle_range' => $start->format('j M') . ' - ' . $end->format('j M Y'),
|
|
'base_salary' => $baseSalary,
|
|
'prorated_base' => round($proratedBase, 2),
|
|
'is_prorated' => round($proratedBase, 2) < round($baseSalary, 2),
|
|
'salary_type' => $staff->salary_type,
|
|
'commission_count' => $commissionCount,
|
|
'commission_amount' => $commissionAmount,
|
|
'total_commission' => $totalCommission,
|
|
'advance_deduction' => $advanceDeduction,
|
|
'remaining_advance' => $remainingAdvance,
|
|
'net_payable' => round($netPayable, 2)
|
|
]);
|
|
}
|
|
|
|
public function settleSalary(Request $request, $id)
|
|
{
|
|
$staff = Staff::findOrFail($id);
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
if ($user && $user->isReceptionist() && $staff->branch_id != $user->branch_id) {
|
|
return response()->json(['message' => 'Unauthorized'], 403);
|
|
}
|
|
$details = $this->getSettlementDetails($request, $id)->getData(true);
|
|
|
|
if (!($details['can_settle'] ?? false)) {
|
|
return response()->json(['message' => $details['message'] ?? 'Cannot settle at this time'], 400);
|
|
}
|
|
|
|
$settlementMonth = $details['settlement_month'];
|
|
$batchId = $request->input('batch_id');
|
|
|
|
// Double check for existing record
|
|
$exists = \App\Models\StaffPayment::where('staff_id', $id)
|
|
->where('payment_type', 'Salary Settlement')
|
|
->where('settlement_month', $settlementMonth)
|
|
->exists();
|
|
|
|
if ($exists) {
|
|
return response()->json(['message' => 'Salary for this period is already settled'], 400);
|
|
}
|
|
|
|
$payment = \App\Models\StaffPayment::create([
|
|
'staff_id' => $id,
|
|
'amount' => $details['net_payable'],
|
|
'payment_date' => Carbon::now()->toDateString(),
|
|
'payment_type' => 'Salary Settlement',
|
|
'settlement_month' => $settlementMonth,
|
|
'status' => 'Paid',
|
|
'remarks' => $request->input('remarks') ?? "Salary settlement for {$details['period']}",
|
|
'batch_id' => $batchId
|
|
]);
|
|
|
|
// Log to Accounts (Debit)
|
|
\App\Models\Account::create([
|
|
'date' => Carbon::now()->toDateString(),
|
|
'time' => Carbon::now()->toTimeString(),
|
|
'credit' => 0,
|
|
'debit' => $details['net_payable'],
|
|
'type' => 'salary',
|
|
'accountable_id' => $payment->id,
|
|
'accountable_type' => \App\Models\StaffPayment::class,
|
|
'description' => "Salary settlement for {$staff->full_name} ({$details['period']})"
|
|
]);
|
|
|
|
// Log to General Expenses
|
|
$salaryCategory = \App\Models\ExpenseCategory::where('name', 'Salary')->first();
|
|
\App\Models\Expense::create([
|
|
'date' => Carbon::now()->toDateString(),
|
|
'branch_id' => $staff->branch_id,
|
|
'expense_category_id' => $salaryCategory ? $salaryCategory->id : 1,
|
|
'expense_type' => 'Account',
|
|
'amount' => $details['net_payable'],
|
|
'remarks' => "Salary payout for {$staff->full_name} ({$details['period']})" . ($batchId ? " (Batch: {$batchId})" : ""),
|
|
'batch_id' => $batchId
|
|
]);
|
|
|
|
// Update Advance Deduction if applicable
|
|
$deduction = SalaryAdvanceDeduction::where('staff_id', $id)
|
|
->where('status', 'Pending')
|
|
->first();
|
|
|
|
if ($deduction) {
|
|
$newRemaining = $deduction->remaining_amount - $details['advance_deduction'];
|
|
$newPaid = ($deduction->paid_amount ?: 0) + $details['advance_deduction'];
|
|
$deduction->update([
|
|
'remaining_amount' => max(0, $newRemaining),
|
|
'paid_amount' => $newPaid,
|
|
'status' => $newRemaining <= 0.01 ? 'Closed' : 'Pending'
|
|
]);
|
|
|
|
if ($newRemaining <= 0.01) {
|
|
$staff->update(['advance_enabled' => false]);
|
|
}
|
|
}
|
|
|
|
return response()->json(['message' => 'Salary settled successfully', 'payment' => $payment]);
|
|
}
|
|
|
|
private function recordFinancialsForAdvance($staff, $amount)
|
|
{
|
|
if ($amount <= 0) return;
|
|
|
|
// Create a payment record for the advance
|
|
$payment = \App\Models\StaffPayment::create([
|
|
'staff_id' => $staff->id,
|
|
'amount' => $amount,
|
|
'payment_date' => Carbon::now()->toDateString(),
|
|
'payment_type' => 'Advance',
|
|
'status' => 'Paid',
|
|
'remarks' => "Salary advance issued"
|
|
]);
|
|
|
|
// Log to Accounts (Debit)
|
|
\App\Models\Account::create([
|
|
'date' => Carbon::now()->toDateString(),
|
|
'time' => Carbon::now()->toTimeString(),
|
|
'credit' => 0,
|
|
'debit' => $amount,
|
|
'type' => 'salary',
|
|
'accountable_id' => $payment->id,
|
|
'accountable_type' => \App\Models\StaffPayment::class,
|
|
'description' => "Salary advance for {$staff->full_name}"
|
|
]);
|
|
|
|
// Log to General Expenses
|
|
$advanceCategory = \App\Models\ExpenseCategory::where('name', 'Salary Advance')->first();
|
|
\App\Models\Expense::create([
|
|
'date' => Carbon::now()->toDateString(),
|
|
'branch_id' => $staff->branch_id,
|
|
'expense_category_id' => $advanceCategory ? $advanceCategory->id : 1,
|
|
'expense_type' => 'Account',
|
|
'amount' => $amount,
|
|
'remarks' => "Salary advance issued to {$staff->full_name}"
|
|
]);
|
|
}
|
|
|
|
public function getPayrollStatus($id)
|
|
{
|
|
$staff = Staff::findOrFail($id);
|
|
$joiningDate = Carbon::parse($staff->joining_date)->startOfMonth();
|
|
$currentMonth = Carbon::now()->startOfMonth();
|
|
|
|
$payroll = [];
|
|
$tempMonth = $joiningDate->copy();
|
|
|
|
// Get all settlements for this staff
|
|
$settlements = \App\Models\StaffPayment::where('staff_id', $id)
|
|
->where('payment_type', 'Salary Settlement')
|
|
->pluck('amount', 'settlement_month')
|
|
->toArray();
|
|
|
|
while ($tempMonth->lessThan($currentMonth)) {
|
|
$monthKey = $tempMonth->format('Y-m');
|
|
$isPaid = isset($settlements[$monthKey]);
|
|
|
|
if (!$isPaid) {
|
|
$payroll[] = [
|
|
'month' => $tempMonth->format('F Y'),
|
|
'month_key' => $monthKey,
|
|
'status' => 'Unpaid',
|
|
'amount' => null,
|
|
'can_settle' => true
|
|
];
|
|
}
|
|
|
|
$tempMonth->addMonth();
|
|
}
|
|
|
|
return response()->json(array_reverse($payroll));
|
|
}
|
|
|
|
public function getCommissionHistory($id)
|
|
{
|
|
$staff = Staff::findOrFail($id);
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
if ($user && $user->isReceptionist() && $staff->branch_id != $user->branch_id) {
|
|
return response()->json(['message' => 'Unauthorized'], 403);
|
|
}
|
|
$history = TrainerCommission::where('staff_id', $id)
|
|
->orderBy('effective_month', 'desc')
|
|
->get();
|
|
return response()->json($history);
|
|
}
|
|
|
|
public function getAdvanceHistory($id)
|
|
{
|
|
$staff = Staff::findOrFail($id);
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
if ($user && $user->isReceptionist() && $staff->branch_id != $user->branch_id) {
|
|
return response()->json(['message' => 'Unauthorized'], 403);
|
|
}
|
|
$history = SalaryAdvanceDeduction::where('staff_id', $id)
|
|
->orderBy('created_at', 'desc')
|
|
->get();
|
|
return response()->json($history);
|
|
}
|
|
|
|
public function getAllPendingSalaries(Request $request)
|
|
{
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
$branchId = $user && $user->isReceptionist() ? $user->branch_id : $request->query('branch_id');
|
|
|
|
$query = Staff::with('branch');
|
|
if ($branchId) {
|
|
$query->where('branch_id', $branchId);
|
|
}
|
|
$staffMembers = $query->get();
|
|
|
|
$pending = [];
|
|
|
|
foreach ($staffMembers as $staff) {
|
|
$branch = $staff->branch;
|
|
$joiningDate = Carbon::parse($staff->joining_date)->startOfMonth();
|
|
$tempMonth = $joiningDate->copy();
|
|
|
|
// Get all settlements for this staff
|
|
$settlements = \App\Models\StaffPayment::where('staff_id', $staff->id)
|
|
->where('payment_type', 'Salary Settlement')
|
|
->pluck('amount', 'settlement_month')
|
|
->toArray();
|
|
|
|
$staffMonths = [];
|
|
|
|
// Loop while generation date for the month has passed
|
|
while (true) {
|
|
$monthKey = $tempMonth->format('Y-m');
|
|
|
|
// Calculate generation date for this month
|
|
$genDay = $branch->salary_generation_day ?? 2;
|
|
$generationDate = $tempMonth->copy()->addMonth()->day(min($genDay, $tempMonth->copy()->addMonth()->daysInMonth));
|
|
|
|
if (Carbon::now()->lessThan($generationDate)) {
|
|
break;
|
|
}
|
|
|
|
if (!isset($settlements[$monthKey])) {
|
|
// Calculate details for this month
|
|
[$start, $end] = $this->getSalaryCycleRange($monthKey, $branch);
|
|
|
|
$baseSalary = $staff->salary_amount ?: 0;
|
|
$proratedBase = $this->calculateProRatedSalary($baseSalary, $start, $end, $staff->joining_date);
|
|
|
|
// Commission for this month
|
|
$commission = TrainerCommission::where('staff_id', $staff->id)
|
|
->where('effective_month', '<=', $monthKey)
|
|
->orderBy('effective_month', 'desc')
|
|
->first();
|
|
$commissionAmount = $commission ? $commission->total_amount : 0;
|
|
|
|
// Advance deduction
|
|
$advanceDed = 0;
|
|
if ($staff->advance_enabled) {
|
|
if ($staff->advance_repayment_mode === 'Full Next Month') {
|
|
$advanceDed = $staff->advance_amount;
|
|
} else {
|
|
$deduction = SalaryAdvanceDeduction::where('staff_id', $staff->id)->where('status', 'Pending')->first();
|
|
$advanceDed = $deduction ? $deduction->monthly_deduction : 0;
|
|
}
|
|
}
|
|
|
|
$netPayable = $proratedBase + $commissionAmount - $advanceDed;
|
|
|
|
$staffMonths[] = [
|
|
'month_key' => $monthKey,
|
|
'month_name' => $tempMonth->format('F Y'),
|
|
'cycle_range' => $start->format('j M') . ' - ' . $end->format('j M'),
|
|
'base_salary' => $baseSalary,
|
|
'prorated_base' => round($proratedBase, 2),
|
|
'commission' => $commissionAmount,
|
|
'advance_deduction' => $advanceDed,
|
|
'net_payable' => round($netPayable, 2),
|
|
];
|
|
}
|
|
$tempMonth->addMonth();
|
|
}
|
|
|
|
if (!empty($staffMonths)) {
|
|
$totalNet = array_sum(array_column($staffMonths, 'net_payable'));
|
|
$totalBase = array_sum(array_column($staffMonths, 'prorated_base'));
|
|
$totalComm = array_sum(array_column($staffMonths, 'commission'));
|
|
$totalAdv = array_sum(array_column($staffMonths, 'advance_deduction'));
|
|
|
|
$pending[] = [
|
|
'staff_id' => $staff->id,
|
|
'staff_name' => $staff->full_name,
|
|
'branch' => $staff->branch?->name ?? 'N/A',
|
|
'role' => $staff->designation ?? 'Staff',
|
|
'base_salary' => $totalBase,
|
|
'commission' => $totalComm,
|
|
'advance_ded' => $totalAdv,
|
|
'net_payable' => $totalNet,
|
|
'status' => 'Pending',
|
|
'pending_months' => $staffMonths
|
|
];
|
|
}
|
|
}
|
|
|
|
return response()->json($pending);
|
|
}
|
|
|
|
public function bulkSettleSalaries(Request $request)
|
|
{
|
|
$payload = $request->validate([
|
|
'settlements' => 'required|array',
|
|
'settlements.*.staff_id' => 'required|exists:staff,id',
|
|
'settlements.*.month_key' => 'required|string',
|
|
'remarks' => 'nullable|string'
|
|
]);
|
|
|
|
$batchId = 'BATCH-' . strtoupper(bin2hex(random_bytes(4)));
|
|
$results = [];
|
|
foreach ($payload['settlements'] as $item) {
|
|
$settleReq = new Request();
|
|
$settleReq->merge([
|
|
'month' => $item['month_key'],
|
|
'batch_id' => $batchId,
|
|
'remarks' => $payload['remarks'] ?? 'Bulk salary release'
|
|
]);
|
|
|
|
try {
|
|
$resp = $this->settleSalary($settleReq, $item['staff_id']);
|
|
$results[] = [
|
|
'staff_id' => $item['staff_id'],
|
|
'month' => $item['month_key'],
|
|
'status' => 'success'
|
|
];
|
|
} catch (\Exception $e) {
|
|
Log::error("Bulk settle error for staff {$item['staff_id']}: " . $e->getMessage());
|
|
$results[] = [
|
|
'staff_id' => $item['staff_id'],
|
|
'month' => $item['month_key'],
|
|
'status' => 'error',
|
|
'message' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
return response()->json(['results' => $results, 'batch_id' => $batchId]);
|
|
}
|
|
private function getSalaryCycleRange($monthKey, $branch)
|
|
{
|
|
$fromDay = $branch->payroll_from_day ?? 1;
|
|
$toDay = $branch->payroll_to_day ?? 28;
|
|
|
|
$month = Carbon::parse($monthKey . '-01');
|
|
|
|
if ($fromDay <= $toDay) {
|
|
// Same month cycle (e.g., 1st to 28th)
|
|
$start = $month->copy()->day($fromDay)->startOfDay();
|
|
$end = $month->copy()->day($toDay)->endOfDay();
|
|
} else {
|
|
// Cross-month cycle (e.g., 25th to 24th next month)
|
|
// If monthKey is Feb, cycle is Jan 25 to Feb 24
|
|
$end = $month->copy()->day($toDay)->endOfDay();
|
|
$start = $month->copy()->subMonth()->day($fromDay)->startOfDay();
|
|
}
|
|
|
|
return [$start, $end];
|
|
}
|
|
|
|
private function calculateProRatedSalary($monthlySalary, $start, $end, $joiningDate)
|
|
{
|
|
$joining = Carbon::parse($joiningDate)->startOfDay();
|
|
|
|
// If joined after the cycle ended, salary is 0
|
|
if ($joining->greaterThan($end)) return 0;
|
|
|
|
// If joined before or on the start day, full salary
|
|
if ($joining->lessThanOrEqualTo($start)) return $monthlySalary;
|
|
|
|
// Prorated calculation
|
|
$totalDaysInCycle = $start->diffInDays($end) + 1;
|
|
$workDays = $joining->diffInDays($end) + 1;
|
|
|
|
return ($monthlySalary / $totalDaysInCycle) * $workDays;
|
|
}
|
|
}
|