feat: implement employee advance payment management with new model, migration, and frontend updates
This commit is contained in:
parent
fe1549e0a5
commit
414ce51904
@ -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(
|
||||
|
||||
56
app/Models/EmployeeAdvancePayment.php
Normal file
56
app/Models/EmployeeAdvancePayment.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
#[Guarded(['id'])]
|
||||
#[Appends([
|
||||
'amount_formatted',
|
||||
'paid_at_formatted',
|
||||
])]
|
||||
class EmployeeAdvancePayment extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'amount' => '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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::statement("ALTER TABLE employee_advances MODIFY COLUMN status ENUM('pending','approved','rejected','partially_paid','paid') DEFAULT 'pending'");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::statement("ALTER TABLE employee_advances MODIFY COLUMN status ENUM('pending','approved','rejected','paid') DEFAULT 'pending'");
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('employee_advance_payments', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -44,6 +44,7 @@ const emit = defineEmits<{
|
||||
{{ description }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<slot name="content" />
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel :disabled="loading">
|
||||
{{ cancelLabel }}
|
||||
|
||||
@ -7,12 +7,17 @@ defineProps<{
|
||||
tooltip?: string;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
click: [];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="size-8" :disabled="disabled">
|
||||
<Button variant="ghost" size="icon" class="size-8" :disabled="disabled"
|
||||
@click="$emit('click')">
|
||||
<Banknote class="size-4" />
|
||||
<span class="sr-only">{{ tooltip || 'Bayar' }}</span>
|
||||
</Button>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -64,9 +64,11 @@ function approveEmployeeAdvance() {
|
||||
onSuccess: () => {
|
||||
approveConfirmOpen.value = false;
|
||||
},
|
||||
onError: (errors: any) => {
|
||||
if (errors.system) {
|
||||
toast.error(errors.system);
|
||||
onError: (errors: Record<string, string>) => {
|
||||
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<string, string>) => {
|
||||
const message = errors.system ?? Object.values(errors)[0];
|
||||
|
||||
if (message) {
|
||||
toast.error(message);
|
||||
}
|
||||
},
|
||||
onFinish: () => {
|
||||
@ -146,8 +152,21 @@ function payEmployeeAdvance() {
|
||||
<Field>
|
||||
<FieldLabel for="pay-amount" required>Jumlah Pembayaran</FieldLabel>
|
||||
<RupiahInput id="pay-amount" v-model="payAmount" placeholder="0" />
|
||||
<FieldError v-if="payError" :errors="{ amount: [payError] }" />
|
||||
<FieldError v-if="payError" :errors="[payError]" />
|
||||
</Field>
|
||||
|
||||
<div v-if="employeeAdvance.payments && employeeAdvance.payments.length > 0" class="space-y-2">
|
||||
<p class="text-sm font-medium">Riwayat Pembayaran</p>
|
||||
<div class="max-h-40 space-y-2 overflow-y-auto rounded-md border p-2">
|
||||
<div v-for="payment in employeeAdvance.payments" :key="payment.id"
|
||||
class="flex items-center justify-between text-sm">
|
||||
<div>
|
||||
<p class="font-medium">{{ payment.amount_formatted }}</p>
|
||||
<p class="text-xs text-muted-foreground">{{ payment.paid_at_formatted }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
|
||||
@ -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 = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user