diff --git a/app/Http/Controllers/Admin/Finance/EmployeeAdvanceController.php b/app/Http/Controllers/Admin/Finance/EmployeeAdvanceController.php index 2c17650..1c36e89 100644 --- a/app/Http/Controllers/Admin/Finance/EmployeeAdvanceController.php +++ b/app/Http/Controllers/Admin/Finance/EmployeeAdvanceController.php @@ -4,6 +4,7 @@ use App\Enums\EmployeeAdvanceStatus; use App\Http\Controllers\Controller; +use App\Http\Requests\Admin\Finance\EmployeeAdvancePaymentRequest; use App\Http\Requests\Admin\Finance\EmployeeAdvanceRequest; use App\Http\Requests\PaginatedRequest; use App\Models\EmployeeAdvance; @@ -68,11 +69,11 @@ public function approve(EmployeeAdvance $employeeAdvance): RedirectResponse ); } - public function pay(EmployeeAdvance $employeeAdvance): RedirectResponse + public function pay(EmployeeAdvancePaymentRequest $request, EmployeeAdvance $employeeAdvance): RedirectResponse { return $this->handleAction( - fn () => $this->service->pay($employeeAdvance), - 'Kasbon berhasil dibayar.', + fn () => $this->service->pay($employeeAdvance, $request->validated('amount')), + 'Pembayaran kasbon berhasil.', 'admin.finance.employee-advances.index' ); } diff --git a/app/Http/Requests/Admin/Finance/EmployeeAdvancePaymentRequest.php b/app/Http/Requests/Admin/Finance/EmployeeAdvancePaymentRequest.php new file mode 100644 index 0000000..ded071f --- /dev/null +++ b/app/Http/Requests/Admin/Finance/EmployeeAdvancePaymentRequest.php @@ -0,0 +1,37 @@ +merge($this->stripCurrencyDot($this->all(), 'amount')); + } + + public function rules(): array + { + return [ + 'amount' => ['required', 'integer', 'min:1'], + ]; + } + + public function attributes(): array + { + return [ + 'amount' => 'jumlah bayar', + ]; + } +} diff --git a/app/Models/EmployeeAdvance.php b/app/Models/EmployeeAdvance.php index 89df7fe..82161b8 100644 --- a/app/Models/EmployeeAdvance.php +++ b/app/Models/EmployeeAdvance.php @@ -11,9 +11,10 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; -#[Appends(['formatted_amount', 'formatted_created_at', 'formatted_due_date', 'formatted_paid_amount', 'status_label'])] +#[Appends(['formatted_amount', 'formatted_created_at', 'formatted_due_date', 'formatted_paid_amount', 'formatted_remaining_amount', 'status_label'])] #[Guarded(['id'])] class EmployeeAdvance extends Model { @@ -59,6 +60,13 @@ protected function formattedPaidAmount(): Attribute ); } + protected function formattedRemainingAmount(): Attribute + { + return Attribute::make( + get: fn () => 'Rp '.number_format($this->amount - $this->paid_amount, 0, ',', '.'), + ); + } + protected function statusLabel(): Attribute { return Attribute::make( @@ -106,6 +114,11 @@ public function employee(): BelongsTo return $this->belongsTo(Employee::class); } + public function payments(): HasMany + { + return $this->hasMany(EmployeeAdvancePayment::class); + } + public function paidBy(): BelongsTo { return $this->belongsTo(User::class, 'paid_by_id'); diff --git a/app/Models/EmployeeAdvancePayment.php b/app/Models/EmployeeAdvancePayment.php new file mode 100644 index 0000000..e871cec --- /dev/null +++ b/app/Models/EmployeeAdvancePayment.php @@ -0,0 +1,54 @@ + 'integer', + 'paid_at' => 'datetime', + ]; + } + + protected function formattedAmount(): Attribute + { + return Attribute::make( + get: fn() => 'Rp ' . number_format($this->amount, 0, ',', '.'), + ); + } + + protected function formattedPaidAt(): Attribute + { + return Attribute::make( + get: fn() => $this->paid_at?->translatedFormat('l, d F Y'), + ); + } + + public function employeeAdvance(): BelongsTo + { + return $this->belongsTo(EmployeeAdvance::class); + } + + public function paidBy(): BelongsTo + { + return $this->belongsTo(User::class, 'paid_by_id'); + } + + public function cashTransaction(): BelongsTo + { + return $this->belongsTo(CashTransaction::class); + } +} diff --git a/app/Services/Admin/Finance/EmployeeAdvanceService.php b/app/Services/Admin/Finance/EmployeeAdvanceService.php index 9f4aad1..e38a1e9 100644 --- a/app/Services/Admin/Finance/EmployeeAdvanceService.php +++ b/app/Services/Admin/Finance/EmployeeAdvanceService.php @@ -4,11 +4,13 @@ use App\Enums\EmployeeAdvanceStatus; use App\Models\EmployeeAdvance; +use App\Models\EmployeeAdvancePayment; use App\Services\Concerns\HandlesCashTransactions; use App\Services\NotificationService; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; +use Illuminate\Validation\ValidationException; class EmployeeAdvanceService { @@ -22,7 +24,7 @@ private function canViewAll(): bool public function getAll(array $filters = []): Collection { return EmployeeAdvance::select(['id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at']) - ->with(['employee.user.userProfile']) + ->with(['employee.user.userProfile', 'payments.paidBy.userProfile']) ->when(! $this->canViewAll(), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))) ->latest() ->get(); @@ -32,7 +34,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort = { return EmployeeAdvance::query() ->select(['id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at']) - ->with(['employee.user.userProfile']) + ->with(['employee.user.userProfile', 'payments.paidBy.userProfile']) ->when(! $this->canViewAll(), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))) ->when($filters['status'] ?? null, fn ($q) => $q->where('status', $filters['status'])) ->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%")) @@ -117,29 +119,51 @@ public function approve(EmployeeAdvance $employeeAdvance): EmployeeAdvance return $employeeAdvance; } - public function pay(EmployeeAdvance $employeeAdvance): EmployeeAdvance + public function pay(EmployeeAdvance $employeeAdvance, int $amount): EmployeeAdvance { - $employeeAdvance = DB::transaction(function () use ($employeeAdvance) { + $remaining = $employeeAdvance->amount - $employeeAdvance->paid_amount; + + if ($amount > $remaining) { + throw ValidationException::withMessages([ + 'amount' => 'Jumlah bayar melebihi sisa kasbon.', + ]); + } + + $employeeAdvance = DB::transaction(function () use ($employeeAdvance, $amount) { $cashTransaction = $this->creditCash( - amount: $employeeAdvance->amount, + amount: $amount, description: 'Pembayaran kasbon: '.$employeeAdvance->description, ); - $employeeAdvance->update([ - 'status' => EmployeeAdvanceStatus::PAID, + EmployeeAdvancePayment::create([ + 'employee_advance_id' => $employeeAdvance->id, 'paid_by_id' => auth()->id(), - 'paid_amount' => $employeeAdvance->amount, + 'cash_transaction_id' => $cashTransaction->id, + 'amount' => $amount, 'paid_at' => now(), - 'repayment_cash_transaction_id' => $cashTransaction->id, + ]); + + $newPaidAmount = $employeeAdvance->paid_amount + $amount; + $isFullyPaid = $newPaidAmount >= $employeeAdvance->amount; + + $employeeAdvance->update([ + 'paid_amount' => $newPaidAmount, + 'status' => $isFullyPaid ? EmployeeAdvanceStatus::PAID : $employeeAdvance->status, + 'paid_by_id' => $isFullyPaid ? auth()->id() : $employeeAdvance->paid_by_id, + 'paid_at' => $isFullyPaid ? now() : $employeeAdvance->paid_at, ]); return $employeeAdvance; }); + $notificationBody = $employeeAdvance->status === EmployeeAdvanceStatus::PAID + ? 'Kasbon sebesar Rp '.number_format($employeeAdvance->amount, 0, ',', '.').' telah dibayar lunas'.' oleh '.auth()->user()->full_name.'.' + : 'Pembayaran kasbon sebesar Rp '.number_format($amount, 0, ',', '.').' oleh '.auth()->user()->full_name.'. Sisa: Rp '.number_format($employeeAdvance->amount - $employeeAdvance->paid_amount, 0, ',', '.').'.'; + NotificationService::notify( roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'], - title: 'Kasbon Dibayar', - body: 'Kasbon sebesar Rp '.number_format($employeeAdvance->amount, 0, ',', '.').' telah dibayar lunas'.' oleh '.auth()->user()->full_name.'.', + title: $employeeAdvance->status === EmployeeAdvanceStatus::PAID ? 'Kasbon Dibayar' : 'Pembayaran Kasbon', + body: $notificationBody, url: route('admin.finance.employee-advances.index'), additionalUser: $employeeAdvance->employee->user ?? null, ); diff --git a/resources/js/pages/admin/finance/employee-advance/columns.tsx b/resources/js/pages/admin/finance/employee-advance/columns.tsx index fc2f30c..3b08960 100644 --- a/resources/js/pages/admin/finance/employee-advance/columns.tsx +++ b/resources/js/pages/admin/finance/employee-advance/columns.tsx @@ -1,7 +1,21 @@ -import type { ColumnDef } from '@tanstack/react-table'; -import { CheckCircle, CircleDollarSign, Pencil, Trash2 } from 'lucide-react'; import { RowActions } from '@/components/row-actions'; import { Badge } from '@/components/ui/badge'; +import type { ColumnDef } from '@tanstack/react-table'; +import { CheckCircle, CircleDollarSign, History, Pencil, Trash2 } from 'lucide-react'; + +export type EmployeeAdvancePayment = { + id: number; + amount: number; + formatted_amount: string; + description: string | null; + paid_at: string; + formatted_paid_at: string; + paid_by: { + user_profile: { + full_name: string; + }; + } | null; +}; export type EmployeeAdvance = { id: number; @@ -9,6 +23,8 @@ export type EmployeeAdvance = { formatted_amount: string; paid_amount: number; formatted_paid_amount: string; + remaining_amount: number; + formatted_remaining_amount: string; description: string; due_date: string; formatted_due_date: string; @@ -23,6 +39,7 @@ export type EmployeeAdvance = { }; }; }; + payments: EmployeeAdvancePayment[]; }; function getStatusBadge(status: string) { @@ -63,6 +80,7 @@ type CreateColumnsParams = { handleDeleteClick: (employeeAdvance: EmployeeAdvance) => void; handleApprove: (employeeAdvance: EmployeeAdvance) => void; handlePay: (employeeAdvance: EmployeeAdvance) => void; + handleShowPayments: (employeeAdvance: EmployeeAdvance) => void; can: (permission: string) => boolean; authUserId: number; }; @@ -70,7 +88,7 @@ type CreateColumnsParams = { export function createEmployeeAdvanceColumns( params: CreateColumnsParams, ): ColumnDef[] { - const { handleEdit, handleDeleteClick, handleApprove, handlePay, can, authUserId } = + const { handleEdit, handleDeleteClick, handleApprove, handlePay, handleShowPayments, can, authUserId } = params; return [ @@ -98,11 +116,27 @@ export function createEmployeeAdvanceColumns( accessorKey: 'formatted_amount', header: () => Jumlah, cell: ({ row }) => ( - - - {row.getValue('formatted_amount') as string} + + {row.getValue('formatted_amount') as string} ), }, + { + accessorKey: 'formatted_remaining_amount', + header: () => Sisa, + cell: ({ row }) => { + const employeeAdvance = row.original; + if (employeeAdvance.status === 'paid') { + return Lunas; + } + + return ( + + {row.getValue('formatted_remaining_amount') as string} + + ); + }, + }, { accessorKey: 'description', header: () => Keterangan, @@ -161,6 +195,12 @@ export function createEmployeeAdvanceColumns( employeeAdvance.status === 'approved', onClick: () => handlePay(employeeAdvance), }, + { + label: 'Riwayat', + icon: , + show: employeeAdvance.payments.length > 0, + onClick: () => handleShowPayments(employeeAdvance), + }, { label: 'Edit', icon: , diff --git a/resources/js/pages/admin/finance/employee-advance/index.tsx b/resources/js/pages/admin/finance/employee-advance/index.tsx index f1f94c3..fe408b7 100644 --- a/resources/js/pages/admin/finance/employee-advance/index.tsx +++ b/resources/js/pages/admin/finance/employee-advance/index.tsx @@ -8,8 +8,16 @@ import InputError from '@/components/input-error'; import { PageHeader } from '@/components/page-header'; import { RupiahInput } from '@/components/rupiah-input'; import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import { Separator } from '@/components/ui/separator'; import { Select, SelectContent, @@ -66,6 +74,7 @@ export default function EmployeeAdvanceIndex({ const [deleting, setDeleting] = useState(null); const [approving, setApproving] = useState(null); const [paying, setPaying] = useState(null); + const [viewingPayments, setViewingPayments] = useState(null); const [dueDate, setDueDate] = useState(undefined); const [editingDueDate, setEditingDueDate] = useState( undefined, @@ -125,25 +134,12 @@ export default function EmployeeAdvanceIndex({ ); } - function handlePay() { - if (!paying) { - return; - } - - router.post( - pay(paying.id), - {}, - { - onSuccess: () => setPaying(null), - }, - ); - } - const columns = createEmployeeAdvanceColumns({ handleEdit: (employeeAdvance) => setEditing(employeeAdvance), handleDeleteClick: (employeeAdvance) => setDeleting(employeeAdvance), handleApprove: (employeeAdvance) => setApproving(employeeAdvance), handlePay: (employeeAdvance) => setPaying(employeeAdvance), + handleShowPayments: (employeeAdvance) => setViewingPayments(employeeAdvance), can, authUserId: auth.user.id, }); @@ -383,20 +379,128 @@ export default function EmployeeAdvanceIndex({ onConfirm={handleApprove} /> - { if (!open) { setPaying(null); } }} title="Bayar Kasbon" - description={(advance) => - `Apakah Anda yakin ingin membayar kasbon "${advance.description}" sebesar ${advance.formatted_amount}? Saldo kas akan dikembalikan.` + action={paying ? pay(paying.id) : ''} + resetOnSuccess + onSuccess={() => setPaying(null)} + > + {({ errors }) => + paying && ( + <> +
+

+ Sisa:{' '} + + {paying.formatted_remaining_amount} + +

+
+
+ + + +
+ + ) } - confirmLabel="Bayar" - onConfirm={handlePay} - /> + + + { + if (!open) { + setViewingPayments(null); + } + }} + > + + + Riwayat Pembayaran + + {viewingPayments?.description} + + + {viewingPayments && ( +
+
+ + Total: + + + {viewingPayments.formatted_amount} + +
+
+ + Terbayar: + + + {viewingPayments.formatted_paid_amount} + +
+
+ + Sisa: + + + {viewingPayments.formatted_remaining_amount} + +
+ + + + {viewingPayments.payments.length === 0 ? ( +

+ Belum ada pembayaran. +

+ ) : ( +
+ {viewingPayments.payments.map( + (payment) => ( +
+
+ + {payment.formatted_amount} + + + {payment.formatted_paid_at} + +
+ {payment.paid_by && ( +

+ oleh{' '} + {payment.paid_by + .user_profile + .full_name ?? '-'} +

+ )} +
+ ), + )} +
+ )} +
+ )} +
+
);