diff --git a/app/Models/EmployeeAdvance.php b/app/Models/EmployeeAdvance.php index a011f3f..3ff4956 100644 --- a/app/Models/EmployeeAdvance.php +++ b/app/Models/EmployeeAdvance.php @@ -13,6 +13,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; #[Guarded(['id'])] #[Appends([ @@ -71,6 +72,11 @@ public function verifiedBy(): BelongsTo return $this->belongsTo(User::class, 'verified_by_id'); } + public function payments(): HasMany + { + return $this->hasMany(EmployeeAdvancePayment::class)->latest('paid_at'); + } + public function amountFormatted(): Attribute { return Attribute::make( diff --git a/app/Models/EmployeeAdvancePayment.php b/app/Models/EmployeeAdvancePayment.php new file mode 100644 index 0000000..4a35646 --- /dev/null +++ b/app/Models/EmployeeAdvancePayment.php @@ -0,0 +1,56 @@ + 'integer', + 'paid_at' => 'datetime', + ]; + } + + 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); + } + + public function amountFormatted(): \Illuminate\Database\Eloquent\Casts\Attribute + { + return \Illuminate\Database\Eloquent\Casts\Attribute::make( + get: fn () => 'Rp '.number_format($this->amount, 0, ',', '.'), + ); + } + + public function paidAtFormatted(): \Illuminate\Database\Eloquent\Casts\Attribute + { + return \Illuminate\Database\Eloquent\Casts\Attribute::make( + get: fn () => $this->paid_at?->translatedFormat('l, d F Y H:i'), + ); + } +} diff --git a/app/Services/Finance/EmployeeAdvanceService.php b/app/Services/Finance/EmployeeAdvanceService.php index a73d0da..6bc764d 100644 --- a/app/Services/Finance/EmployeeAdvanceService.php +++ b/app/Services/Finance/EmployeeAdvanceService.php @@ -5,6 +5,7 @@ use App\Enums\EmployeeAdvanceStatus; use App\Enums\Role; use App\Models\EmployeeAdvance; +use App\Models\EmployeeAdvancePayment; use App\Models\User; use App\Services\Concerns\ResolvesAuthenticatedEmployee; use App\Services\System\PushNotificationService; @@ -49,7 +50,7 @@ public function outstandingSummary(?User $user = null): array public function paginateForIndex(array $tableQuery, User $user, string $status = ''): LengthAwarePaginator { $query = EmployeeAdvance::query() - ->with(['employee.user.profile', 'rejection']) + ->with(['employee.user.profile', 'rejection', 'payments']) ->when(! $user->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]), function (Builder $query) use ($user): void { $employeeId = $user->employee?->id ?? -1; $query->where('employee_id', $employeeId); @@ -275,6 +276,15 @@ public function pay(EmployeeAdvance $employeeAdvance, User $user, ?int $payAmoun 'paid_by_id' => $isFullPayment ? $user->id : $employeeAdvance->paid_by_id, 'status' => $isFullPayment ? EmployeeAdvanceStatus::PAID : EmployeeAdvanceStatus::PARTIALLY_PAID, ]); + + EmployeeAdvancePayment::create([ + 'employee_advance_id' => $employeeAdvance->id, + 'paid_by_id' => $user->id, + 'cash_transaction_id' => $cashTransaction->id, + 'amount' => $amountToPay, + 'description' => $description, + 'paid_at' => now(), + ]); }); } catch (ValidationException $e) { throw $e; diff --git a/database/migrations/2026_06_28_003803_add_partially_paid_to_employee_advances_table.php b/database/migrations/2026_06_28_003803_add_partially_paid_to_employee_advances_table.php new file mode 100644 index 0000000..b5102a2 --- /dev/null +++ b/database/migrations/2026_06_28_003803_add_partially_paid_to_employee_advances_table.php @@ -0,0 +1,17 @@ +id(); + + $table->foreignId('employee_advance_id')->constrained()->cascadeOnDelete(); + $table->foreignId('paid_by_id')->nullable()->constrained('users')->restrictOnDelete(); + $table->foreignId('cash_transaction_id')->nullable()->unique()->constrained('cash_transactions')->restrictOnDelete(); + + $table->unsignedBigInteger('amount'); + $table->string('description', 100)->nullable(); + $table->timestamp('paid_at')->useCurrent(); + + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate(); + }); + } + + public function down(): void + { + Schema::dropIfExists('employee_advance_payments'); + } +}; diff --git a/resources/js/components/ConfirmDialog.vue b/resources/js/components/ConfirmDialog.vue index cb554d1..9e08226 100644 --- a/resources/js/components/ConfirmDialog.vue +++ b/resources/js/components/ConfirmDialog.vue @@ -44,6 +44,7 @@ const emit = defineEmits<{ {{ description }} + {{ cancelLabel }} diff --git a/resources/js/components/button/RowPayAction.vue b/resources/js/components/button/RowPayAction.vue index 3a2475b..88a8d4b 100644 --- a/resources/js/components/button/RowPayAction.vue +++ b/resources/js/components/button/RowPayAction.vue @@ -7,12 +7,17 @@ defineProps<{ tooltip?: string; disabled?: boolean; }>(); + +defineEmits<{ + click: []; +}>(); - + {{ tooltip || 'Bayar' }} diff --git a/resources/js/pages/admin/finance/employee-advances/table/columns.ts b/resources/js/pages/admin/finance/employee-advances/table/columns.ts index 0cf4f09..4061478 100644 --- a/resources/js/pages/admin/finance/employee-advances/table/columns.ts +++ b/resources/js/pages/admin/finance/employee-advances/table/columns.ts @@ -42,6 +42,25 @@ export function createColumns( enableSorting: true, header: () => h(DataTableColumnHeader, { title: 'Jumlah', column: 'amount' }), }, + { + id: 'payment_status', + enableSorting: false, + header: () => 'Dibayar / Sisa', + cell: ({ row }) => { + const { paid_amount_formatted, remaining_amount, remaining_amount_formatted, status } = row.original; + + if (status === 'pending' || status === 'rejected') { + return h('span', { class: 'text-muted-foreground text-sm' }, '-'); + } + + return h('div', { class: 'space-y-0.5' }, [ + h('div', { class: 'text-sm font-medium' }, paid_amount_formatted), + remaining_amount > 0 + ? h('div', { class: 'text-xs text-muted-foreground' }, `Sisa: ${remaining_amount_formatted}`) + : h('div', { class: 'text-xs text-green-600 font-medium' }, 'Lunas'), + ]); + }, + }, { accessorKey: 'description', enableSorting: true, diff --git a/resources/js/pages/admin/finance/employee-advances/table/data-table-actions.vue b/resources/js/pages/admin/finance/employee-advances/table/data-table-actions.vue index f7b73d0..bea4f40 100644 --- a/resources/js/pages/admin/finance/employee-advances/table/data-table-actions.vue +++ b/resources/js/pages/admin/finance/employee-advances/table/data-table-actions.vue @@ -64,9 +64,11 @@ function approveEmployeeAdvance() { onSuccess: () => { approveConfirmOpen.value = false; }, - onError: (errors: any) => { - if (errors.system) { - toast.error(errors.system); + onError: (errors: Record) => { + const message = errors.system ?? Object.values(errors)[0]; + + if (message) { + toast.error(message); } }, onFinish: () => { @@ -80,11 +82,13 @@ function payEmployeeAdvance() { if (!amount || amount <= 0) { payError.value = 'Jumlah pembayaran harus lebih dari 0.'; + return; } if (amount > props.employeeAdvance.remaining_amount) { payError.value = 'Jumlah pembayaran melebihi sisa kasbon.'; + return; } @@ -96,9 +100,11 @@ function payEmployeeAdvance() { onSuccess: () => { payConfirmOpen.value = false; }, - onError: (errors: any) => { - if (errors.system) { - toast.error(errors.system); + onError: (errors: Record) => { + const message = errors.system ?? Object.values(errors)[0]; + + if (message) { + toast.error(message); } }, onFinish: () => { @@ -146,8 +152,21 @@ function payEmployeeAdvance() { Jumlah Pembayaran - + + + + Riwayat Pembayaran + + + + {{ payment.amount_formatted }} + {{ payment.paid_at_formatted }} + + + + diff --git a/resources/js/types/employee-advance.ts b/resources/js/types/employee-advance.ts index e168152..b4b3410 100644 --- a/resources/js/types/employee-advance.ts +++ b/resources/js/types/employee-advance.ts @@ -1,3 +1,15 @@ +export type EmployeeAdvancePayment = { + id: number; + employee_advance_id: number; + paid_by_id: number | null; + cash_transaction_id: number | null; + amount: number; + amount_formatted: string; + description: string | null; + paid_at: string; + paid_at_formatted: string; +}; + export type EmployeeAdvanceListItem = { id: number; employee_id: number; @@ -18,6 +30,7 @@ export type EmployeeAdvanceListItem = { is_editable: boolean; can_verify: boolean; can_pay: boolean; + payments?: EmployeeAdvancePayment[]; }; export type EmployeeAdvanceFormData = {
Riwayat Pembayaran
{{ payment.amount_formatted }}
{{ payment.paid_at_formatted }}