feat: enhance EmployeeAdvanceService and ExpenseService with validation and transaction handling; update transaction columns for improved display

This commit is contained in:
Yoga Pangestu 2026-08-06 17:12:22 +07:00
parent 42e04673c6
commit 8df17601ea
5 changed files with 107 additions and 41 deletions

View File

@ -48,7 +48,9 @@ public function create(array $data): EmployeeAdvance
$employee = auth()->user()->employee; $employee = auth()->user()->employee;
if (! $employee) { if (! $employee) {
throw new \Exception('Anda tidak terdaftar sebagai karyawan.'); throw ValidationException::withMessages([
'amount' => 'Anda tidak terdaftar sebagai karyawan.',
]);
} }
$employeeAdvance = EmployeeAdvance::create([ $employeeAdvance = EmployeeAdvance::create([
@ -74,6 +76,44 @@ public function create(array $data): EmployeeAdvance
public function update(EmployeeAdvance $employeeAdvance, array $data): EmployeeAdvance public function update(EmployeeAdvance $employeeAdvance, array $data): EmployeeAdvance
{ {
if ($employeeAdvance->status === EmployeeAdvanceStatus::PAID) {
throw ValidationException::withMessages([
'amount' => 'Kasbon yang sudah dibayar tidak dapat diedit.',
]);
}
if ($employeeAdvance->status === EmployeeAdvanceStatus::APPROVED && $employeeAdvance->cash_transaction_id) {
$oldAmount = $employeeAdvance->amount;
$newAmount = $data['amount'];
if ($oldAmount !== $newAmount) {
$employeeAdvance = DB::transaction(function () use ($employeeAdvance, $data, $oldAmount, $newAmount) {
$this->creditCash(
$oldAmount,
'Pembatalan kasbon: '.$employeeAdvance->description,
CashTransactionType::DEPOSIT,
);
$cashTransaction = $this->debitCash(
$newAmount,
'Kasbon: '.$data['description'],
CashTransactionType::EMPLOYEE_ADVANCE,
);
$employeeAdvance->update([
'amount' => $newAmount,
'description' => $data['description'],
'due_date' => $data['due_date'],
'cash_transaction_id' => $cashTransaction->id,
]);
return $employeeAdvance;
});
return $employeeAdvance;
}
}
$employeeAdvance->update([ $employeeAdvance->update([
'amount' => $data['amount'], 'amount' => $data['amount'],
'description' => $data['description'], 'description' => $data['description'],
@ -85,30 +125,60 @@ public function update(EmployeeAdvance $employeeAdvance, array $data): EmployeeA
public function delete(EmployeeAdvance $employeeAdvance): bool public function delete(EmployeeAdvance $employeeAdvance): bool
{ {
if ($employeeAdvance->status === EmployeeAdvanceStatus::APPROVED && $employeeAdvance->cash_transaction_id) { return DB::transaction(function () use ($employeeAdvance) {
$cashAccount = $this->getCashAccount(); if ($employeeAdvance->status === EmployeeAdvanceStatus::APPROVED && $employeeAdvance->cash_transaction_id) {
$newBalance = $cashAccount->balance + $employeeAdvance->amount; $this->creditCash(
$cashAccount->update(['balance' => $newBalance]); $employeeAdvance->amount,
$employeeAdvance->cashTransaction()->delete(); 'Pembatalan kasbon: '.$employeeAdvance->description,
} CashTransactionType::DEPOSIT,
);
$employeeAdvance->cashTransaction()->delete();
}
return $employeeAdvance->delete(); if ($employeeAdvance->status === EmployeeAdvanceStatus::PAID) {
$this->creditCash(
$employeeAdvance->amount,
'Pembatalan kasbon: '.$employeeAdvance->description,
CashTransactionType::DEPOSIT,
);
$employeeAdvance->load('payments');
foreach ($employeeAdvance->payments as $payment) {
if ($payment->cash_transaction_id) {
$this->debitCash(
$payment->amount,
'Pembatalan pembayaran kasbon: '.$employeeAdvance->description,
CashTransactionType::EXPENSE,
);
$payment->cashTransaction()->delete();
}
}
$employeeAdvance->payments()->delete();
}
return $employeeAdvance->delete();
});
} }
public function approve(EmployeeAdvance $employeeAdvance): EmployeeAdvance public function approve(EmployeeAdvance $employeeAdvance): EmployeeAdvance
{ {
$cashTransaction = $this->debitCash( $employeeAdvance = DB::transaction(function () use ($employeeAdvance) {
$employeeAdvance->amount, $cashTransaction = $this->debitCash(
'Kasbon: '.$employeeAdvance->description, $employeeAdvance->amount,
CashTransactionType::EMPLOYEE_ADVANCE 'Kasbon: '.$employeeAdvance->description,
); CashTransactionType::EMPLOYEE_ADVANCE
);
$employeeAdvance->update([ $employeeAdvance->update([
'cash_transaction_id' => $cashTransaction->id, 'cash_transaction_id' => $cashTransaction->id,
'status' => EmployeeAdvanceStatus::APPROVED, 'status' => EmployeeAdvanceStatus::APPROVED,
'verified_by_id' => auth()->id(), 'verified_by_id' => auth()->id(),
'verified_at' => now(), 'verified_at' => now(),
]); ]);
return $employeeAdvance;
});
NotificationService::notify( NotificationService::notify(
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'], roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
@ -135,7 +205,7 @@ public function pay(EmployeeAdvance $employeeAdvance, int $amount): EmployeeAdva
$cashTransaction = $this->creditCash( $cashTransaction = $this->creditCash(
$amount, $amount,
'Pembayaran kasbon: '.$employeeAdvance->description, 'Pembayaran kasbon: '.$employeeAdvance->description,
CashTransactionType::EMPLOYEE_ADVANCE CashTransactionType::DEPOSIT
); );
EmployeeAdvancePayment::create([ EmployeeAdvancePayment::create([

View File

@ -2,6 +2,7 @@
namespace App\Services\Admin\Finance; namespace App\Services\Admin\Finance;
use App\Enums\CashTransactionType;
use App\Models\CashAccount; use App\Models\CashAccount;
use App\Models\Expense; use App\Models\Expense;
use App\Services\Concerns\HandlesCashTransactions; use App\Services\Concerns\HandlesCashTransactions;
@ -175,10 +176,11 @@ public function update(Expense $expense, array $data): Expense
public function delete(Expense $expense): bool public function delete(Expense $expense): bool
{ {
return DB::transaction(function () use ($expense) { return DB::transaction(function () use ($expense) {
$cashAccount = CashAccount::firstOrFail(); $this->creditCash(
amount: $expense->amount,
$newBalance = $cashAccount->balance + $expense->amount; description: 'Pembatalan pengeluaran: '.$expense->description,
$cashAccount->update(['balance' => $newBalance]); type: CashTransactionType::DEPOSIT,
);
// Invalidate receipt cache // Invalidate receipt cache
$media = $expense->getFirstMedia('receipts'); $media = $expense->getFirstMedia('receipts');

View File

@ -2,10 +2,12 @@
namespace App\Services\Admin\Finance; namespace App\Services\Admin\Finance;
use App\Enums\CashTransactionType;
use App\Enums\PayrollPeriodStatus; use App\Enums\PayrollPeriodStatus;
use App\Enums\PayrollStatus; use App\Enums\PayrollStatus;
use App\Models\Payroll; use App\Models\Payroll;
use App\Models\PayrollPeriod; use App\Models\PayrollPeriod;
use App\Services\Concerns\HandlesCashTransactions;
use App\Services\NotificationService; use App\Services\NotificationService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@ -14,6 +16,8 @@
class PayrollPeriodService class PayrollPeriodService
{ {
use HandlesCashTransactions;
private function canViewAll(): bool private function canViewAll(): bool
{ {
return auth()->user()->hasAnyRole(['developer', 'owner', 'direktur', 'admin-toko']); return auth()->user()->hasAnyRole(['developer', 'owner', 'direktur', 'admin-toko']);
@ -138,9 +142,10 @@ public function pay(Payroll $payroll): Payroll
} }
$payroll = DB::transaction(function () use ($payroll) { $payroll = DB::transaction(function () use ($payroll) {
$cashTransaction = $this->creditCash( $cashTransaction = $this->debitCash(
amount: $payroll->total_amount, amount: $payroll->total_amount,
description: 'Pembayaran gaji karyawan', description: 'Pembayaran gaji karyawan',
type: CashTransactionType::EXPENSE,
); );
$payroll->update([ $payroll->update([

View File

@ -1,7 +1,7 @@
import type { ColumnDef } from '@tanstack/react-table';
import { Pencil, Trash2 } from 'lucide-react';
import { ImagePreviewButton } from '@/components/image-preview-button'; import { ImagePreviewButton } from '@/components/image-preview-button';
import { RowActions } from '@/components/row-actions'; import { RowActions } from '@/components/row-actions';
import type { ColumnDef } from '@tanstack/react-table';
import { Pencil, Trash2 } from 'lucide-react';
export type CashTransaction = { export type CashTransaction = {
id: number; id: number;
@ -30,23 +30,12 @@ function getTypeLabel(type: string): string {
deposit: 'Deposit', deposit: 'Deposit',
withdrawal: 'Withdrawal', withdrawal: 'Withdrawal',
expense: 'Pengeluaran', expense: 'Pengeluaran',
transfer: 'Transfer', employee_advance: 'Kasbon',
}; };
return labels[type] ?? type; return labels[type] ?? type;
} }
function getReferenceLabel(type: string): string {
const labels: Record<string, string> = {
'App\\Models\\Expense': 'Pengeluaran',
'App\\Models\\Order': 'Penjualan Tunai',
'App\\Models\\Purchase': 'Belanja',
'App\\Models\\CashAccount': 'Transfer Kas',
};
return labels[type] ?? '-';
}
type CreateColumnsParams = { type CreateColumnsParams = {
handleEdit: (transaction: CashTransaction) => void; handleEdit: (transaction: CashTransaction) => void;
handleDeleteClick: (transaction: CashTransaction) => void; handleDeleteClick: (transaction: CashTransaction) => void;

View File

@ -69,8 +69,8 @@ export function createExpenseColumns(
accessorKey: 'formatted_amount', accessorKey: 'formatted_amount',
header: () => <span>Jumlah</span>, header: () => <span>Jumlah</span>,
cell: ({ row }) => ( cell: ({ row }) => (
<span className="font-medium text-red-600"> <span className="font-medium">
- {row.getValue('formatted_amount') as string} {row.getValue('formatted_amount') as string}
</span> </span>
), ),
}, },